Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swapping rootViewController with animation

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?

like image 298
edc1591 Avatar asked Jan 11 '12 22:01

edc1591


5 Answers

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;
                }];
like image 59
XJones Avatar answered Nov 18 '22 03:11

XJones


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()
})
like image 46
nikolovski Avatar answered Nov 18 '22 03:11

nikolovski


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.

like image 5
nacross Avatar answered Nov 18 '22 03:11

nacross


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.

like image 4
Mischa Avatar answered Nov 18 '22 03:11

Mischa


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];
}];
like image 3
cclogg Avatar answered Nov 18 '22 04:11

cclogg