Swapping rootViewController with animation

20,911

Solution 1

transitionWithView is intended to animate subviews of the specified container view. It is not so simple to animate changing the root view controller. I've spent a long time trying to do it w/o side effects. See:

Animate change of view controllers without using navigation controller stack, subviews or modal controllers?

EDIT: added excerpt from referenced answer

[UIView transitionFromView:self.window.rootViewController.view
                    toView:viewController.view
                  duration:0.65f
                   options:transition
                completion:^(BOOL finished){
                    self.window.rootViewController = viewController;
                }];

Solution 2

I am aware this is a quite old question, however, neither the accepted answer nor the alternatives provided a good solution (when the animation finished the navigation bar of the new root view controller blinked from white to the background color, which is very jarring).

I fixed this by snapshotting the last screen of the first view controller, overlaying it over the second (destination) view controller, and then animating it as I wanted after I set the second view controller as root:

UIView *overlayView = [[UIScreen mainScreen] snapshotViewAfterScreenUpdates:NO];
[self.destinationViewController.view addSubview:overlayView];
self.window.rootViewController = self.destinationViewController;

[UIView animateWithDuration:0.4f delay:0.0f options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
    overlayView.alpha = 0;
} completion:^(BOOL finished) {
    [overlayView removeFromSuperview];
}];

EDIT: Swift 3 version of the code:

let overlayView = UIScreen.main.snapshotView(afterScreenUpdates: false)
destinationViewController.view.addSubview(overlayView)
window.rootViewController = destinationViewController

UIView.animate(withDuration: 0.4, delay: 0, options: .transitionCrossDissolve, animations: {
    overlayView.alpha = 0
}, completion: { finished in
    overlayView.removeFromSuperview()
})

Solution 3

I have found transitionWithView:duration:options:animations:completion: to produce a more reliable result.

[UIView transitionWithView:window
                  duration:0.3
                   options:UIViewAnimationOptionTransitionFlipFromLeft
                animations:^{
                    [fromView removeFromSuperview];
                    [window addSubview:toView];
                    window.rootViewController = toViewController;
                }
                completion:NULL];

If you leave it until the completion block to set the root view controller then during methods such as view(Will/Did)Appear

self.view.window.rootViewController

Will still be set to the previous view controller. This may not be a problem in most situations unless you need to pass on that reference to the rootViewController to other code during those methods as I did.

Solution 4

The currently accepted answer (at the time of writing this answer) isn't ideal because it might not position UI elements like the navigation bar correctly during the animation or lead to other layout glitches as described in Marko Nikolovski's answer. However, pushing a snapshot of the current view controller's view on top of the next view controller's view for animation purposes isn't ideal either because it cannot be used for view controllers in which any kind of animation is happening during the transition to the next view controller as a snapshot is just a static image.

Taking cclogg's basic idea here's my implementation for a smooth transition between two view controllers that doesn't bother about alpha values:

/// Replaces the window's current `rootViewController` with the `newViewController` 
/// passed as a parameter. If `animated`, the window animates the replacement 
/// with a cross dissolve transition.
///
/// - Parameters:
///   - newViewController: The view controller that replaces the current view controller.
///   - animated: Specifies if the transition between the two view controllers is animated.
///   - duration: The transition's duration. (Ignored if `animated == false`)
private func swapCurrentViewController(with newViewController: UIViewController, 
                                       animated: Bool = true, 
                                       duration: TimeInterval = 1) {

    // Get a reference to the window's current `rootViewController` (the "old" one)
    let oldViewController = window?.rootViewController

    // Replace the window's `rootViewController` with the new one
    window?.rootViewController = newViewController

    if animated, let oldView = oldViewController?.view {

        // Add the old view controller's view on top of the new `rootViewController`
        newViewController.view.addSubview(oldView)

        // Remove the old view controller's view in an animated fashion
        UIView.transition(with: window!,
                          duration: duration,
                          options: .transitionCrossDissolve,
                          animations: { oldView.removeFromSuperview() },
                          completion: nil)
    }
}

It's important to replace the window's rootViewController before initiating the transition or animation because that's how the new view controller gets to know its context, i.e. the correct layout margins etc.


💡 This is a way to go if you really need to replace the window's rootViewController for whatever reason. However, I want to point out that from an architectural point of view a far better approach in my opinion is to create a view controller that's solely responsible for handling transitions between the two other view controllers and set that one as the window's rootViewController and then never change it. It's well explained in this answer to a similar question.

Solution 5

The solution that worked for me was a slight modification of Marko Nikolovski's answer. My existing root view controller had an animated spinner on it, so a snapshot looked weird because it froze the animation. In the end, I was able to do the following (inside the current root view controller):

NextRootViewController *nextRootVC = [self.storyboard instantiateViewControllerWithIdentifier:@"NextRootViewController"];

self.view.window.rootViewController = nextRootVC;

[nextRootVC addSubview:self.view];

[UIView animateWithDuration:0.4 delay:0.2 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
    self.view.alpha = 0;
} completion:^(BOOL finished) {
    [self.view removeFromSuperview];
}];
Share:
20,911
edc1591
Author by

edc1591

Mac OS X/iOS Developer (MenuWeather), Skier, Guitarist, Yankee Fan

Updated on September 18, 2020

Comments

  • edc1591
    edc1591 over 3 years

    I'm having a little trouble swapping rootViewControllers with animation. Here's the code that I'm using:

    [UIView transitionWithView:self.window duration:0.8 options:UIViewAnimationOptionTransitionFlipFromRight animations:^{
            self.window.rootViewController = self.navigationController;
        } completion:nil];
    

    It kind of works except that right before the animation, the screen turns to black and then the animation occurs. It looks like the original rootViewController is getting removed right before the animation. Any ideas?

  • edc1591
    edc1591 over 12 years
    It's working fine in other parts of my app. I tried using your code, but I still have the same problem.
  • XJones
    XJones over 12 years
    not sure what you mean by 'it's working fine in other parts...'. The exact same code is? Also, not sure which part of my code you tried. I added the relevant piece to the answer.
  • edc1591
    edc1591 over 12 years
    I'll consider this as a last resort. I'm looking for a slightly more elegant method.
  • edc1591
    edc1591 over 12 years
    I just tried what you posted and now the view changes right before the animation actually occurs.
  • Daryl Teo
    Daryl Teo over 12 years
    @edc1591 maybe (just a guess) the most elegant solution would be a custom "SwapViewController" that performs the animated transition for you.
  • XJones
    XJones over 12 years
    you must be doing something after this that is causing the problem. post the rest of your code after the animation. in general, you need to wait for the animation to complete before making other view changes. this code is correct.
  • edc1591
    edc1591 over 12 years
    Yeah that's probably it. I just decided to restructure the app a lithe bit so this will not be an issue. I'm marking this as accepted because it seems to be the correct solution to the problem. Thanks for the help!
  • XJones
    XJones over 12 years
    well, thanks. the acceptance is nice but I hope you resolve the issue. if you move any post-animation view changes into the completion block, that should help.
  • edc1591
    edc1591 over 12 years
    I think I do have it resolved. I decided that it would be okay to just modally present the view. It seems to be working pretty nicely.
  • Steve
    Steve over 11 years
    This is the only solution that worked properly for me with iOS 6.
  • EralpB
    EralpB over 9 years
    Maybe he is referencing window.rootViewController somewhere from the app later, that's why you may want to keep a vc rvc.
  • Carlos Jiménez
    Carlos Jiménez about 9 years
    You save my day :) Thanks!!
  • S1LENT WARRIOR
    S1LENT WARRIOR almost 8 years
    I liked your idea!
  • phatmann
    phatmann almost 8 years
    I tried a bunch of the other solutions, and this is the smoothest animation I found. I also like the simplicity of the approach.
  • Zonily Jame
    Zonily Jame over 7 years
    Thanks a bunch for this. This is the best replacement for UIView.transitionFromView I've ever found
  • Sourav Chandra
    Sourav Chandra about 7 years
    Thanks mate! Easy and subtle!
  • Nicolai Harbo
    Nicolai Harbo over 3 years
    This answer deserved more upvotes :D Works like a charm, and solves the glitch, where the window will kinda animate from the top left corner and down, instead of crossdissolving. If I could upvote more than once, I would have done it ;)