Swap rootViewController with animation?
Asked Answered
E

8

98

Im trying to swap to another root view controller with a tab bar; via app delegate, and I want to add transition animation. By default it would only show the view without any animation.

let tabBar = self.instantiateViewController(storyBoard: "Main", viewControllerID: "MainTabbar")
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window = UIWindow(frame: UIScreen.main.bounds)
appDelegate.window?.rootViewController = tabBar
appDelegate.window?.makeKeyAndVisible()

That's how I swapped to another rootview controller.

Etymon answered 14/12, 2016 at 14:1 Comment(0)
T
229

You can use UIView.transition(with: view) to replace the rootViewController of a UIWindow:

guard let window = UIApplication.shared.keyWindow else {
    return
}

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "MainTabbar")

// Set the new rootViewController of the window.
// Calling "UIView.transition" below will animate the swap.
window.rootViewController = vc

// A mask of options indicating how you want to perform the animations.
let options: UIView.AnimationOptions = .transitionCrossDissolve

// The duration of the transition animation, measured in seconds.
let duration: TimeInterval = 0.3

// Creates a transition animation.
// Though `animations` is optional, the documentation tells us that it must not be nil. ¯\_(ツ)_/¯
UIView.transition(with: window, duration: duration, options: options, animations: {}, completion:
{ completed in
    // maybe do something on completion here
})
Thury answered 14/12, 2016 at 14:17 Comment(5)
I get a slight animation glitch with this. The rootViewController.view.frame doesn't include the status bar's frame for me. Had to manually adjust the frame to take into account the status bar taking up the top 20 pixels.Chape
Just pointing out: the options paramater is doing nothing here.Ossify
@MarekStaňa are you talking about .transitionCrossDissolve? you might want do increase the duration if you can't see the cross-fade. 0.3 seconds can easily be missed. I just tested it - and still works :)Thury
How does this work since the animations block is empty? It's working I'm just confused as to how...Edirne
@Shredder2794 If you have a look at the transitionWithView function of UIView (eg. github.com/BigZaphod/Chameleon/blob/master/UIKit/Classes/…) you can see there is happening quite a lot even when there is no content in the animation block. The animation seems to be done by the layer of the animated view. (github.com/BigZaphod/Chameleon/blob/master/UIKit/Classes/…)Thury
W
33

Swift 4

Paste function into AppDelegate:

func setRootViewController(_ vc: UIViewController, animated: Bool = true) {
    guard animated, let window = self.window else {
        self.window?.rootViewController = vc
        self.window?.makeKeyAndVisible()
        return
    }

    window.rootViewController = vc
    window.makeKeyAndVisible()
    UIView.transition(with: window,
                      duration: 0.3,
                      options: .transitionCrossDissolve,
                      animations: nil,
                      completion: nil)
}
Wiencke answered 25/1, 2019 at 12:39 Comment(2)
what if i am using both appdelegate and scenedelegate?Ram
Although, the method is types so that animations is Optional, in fact, this code is not supposed to work if you actually pass nil. It does work if you pass an empty closure though.Cablegram
H
18

Im trying to swap to another root view controller ... and I want to add transition animation

I have an app that does this: it changes the root view controller with animation (it's called Albumen).

But my app actually doesn't actually change the root view controller. The root view controller is a custom container view controller that never changes. Its view is never seen and it has no functionality. Its only job is to be the place where the change happens: it swaps one child view controller for another — and thus the transition animation works.

In other words, you add one view controller to your view controller hierarchy, right at the top of the hierarchy, and the whole problem is solved neatly and correctly.

Horsehide answered 14/12, 2016 at 14:25 Comment(2)
fantastic idea, I got this working in SwiftUI and it made my life much better!Neper
Simple and effective! Thanks!Jigger
N
18

An alternative solution:

let stb = UIStoryboard(name: "YOUR_STORYBOARD_NAME", bundle: nil)
let rootVC = stb.instantiateViewController(withIdentifier: "YOUR_TABBAR_VIEWCONTROLLER_NAME")
let snapshot = (UIApplication.shared.keyWindow?.snapshotView(afterScreenUpdates: true))!
rootVC.view.addSubview(snapshot)

UIApplication.shared.keyWindow?.rootViewController = rootVC
UIView.transition(with: snapshot, 
                  duration: 0.4,
                  options: .transitionCrossDissolve,
                  animations: { 
                      snapshot.layer.opacity = 0
                  },
                  completion: { status in 
                      snapshot.removeFromSuperview()
                  })
Norvall answered 19/9, 2017 at 6:32 Comment(0)
T
9

Try this:

UIView.transition(from: appdelegate.window.rootViewController!.view, to: tabbar.view, duration: 0.6, options: [.transitionCrossDissolve], completion: {
    _ in
    appdelegate.window.rootViewController = tabbar
})
Theone answered 14/12, 2016 at 14:6 Comment(2)
This actually works just fine. Although I wish provided animation types had more variety.Ivyiwis
Great and Easy peasy!From
D
7

Updated Swift 5.3 version:

    let foregroundedScenes = UIApplication.shared.connectedScenes.filter { $0.activationState == .foregroundActive }
    let window = foregroundedScenes.map { $0 as? UIWindowScene }.compactMap { $0 }.first?.windows.filter { $0.isKeyWindow }.first
    
    guard let uWindow = window else { return }

    uWindow.rootViewController = customTabBarController
    UIView.transition(with: uWindow, duration: 0.3, options: [.transitionCrossDissolve], animations: {}, completion: nil)
}
Difficulty answered 9/4, 2021 at 15:16 Comment(0)
A
2

And here is example of transitionCrossDissolve with transform translation Y of snapshotView, I think this looks better than regular transition animation.

Tested with Swift 4~5, iOS 11 ~ 15.7

if let window = UIApplication.shared.keyWindow {
    
    var snapShot = UIView()
    
    let destinationVC = UIViewController()
    if let realSnapShot = window.snapshotView(afterScreenUpdates: true) {
        snapShot = realSnapShot
    }
    destinationVC.view.addSubview(snapShot)
    window.rootViewController = destinationVC
    window.makeKeyAndVisible()
    
    UIView.transition(
        with: window,
        duration: 0.5,
        options: .transitionCrossDissolve,
        animations: {
            snapShot.transform = CGAffineTransform(translationX: 0, y: snapShot.frame.height)
        },
        completion: { status in
            snapShot.removeFromSuperview()
        }
    )
}
Alceste answered 13/10, 2022 at 11:25 Comment(0)
R
0

I have created a helper class for this based on d.felber's answer:


    import UIKit

    class ViewPresenter {

        public static func replaceRootView(for viewController: UIViewController,
                                   duration: TimeInterval = 0.3,
                                   options: UIView.AnimationOptions = .transitionCrossDissolve,
                                   completion: ((Bool) -> Void)? = nil) {
            guard let window = UIApplication.shared.keyWindow else {
                return
            }

            guard let rootViewController = window.rootViewController else {
                return
            }

            viewController.view.frame = rootViewController.view.frame
            viewController.view.layoutIfNeeded()

            UIView.transition(with: window, duration: duration, options: options, animations: {
                window.rootViewController = viewController
            }, completion: completion)
        }
    }

You can use it like this:

    let loginVC = SignInViewController(nibName: "SignInViewController", bundle: nil)
    ViewPresenter.replaceRootView(for: loginVC)

or

ViewPresenter.replaceRootView(for: loginVC, duration: 0.3, options: .transitionCrossDissolve) { 
(bool) in
       // do something
}
Renter answered 24/6, 2019 at 9:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.