127

Is there any way to have a Transition/animation effect while replacing an existing viewcontroller as rootviewcontroller with a new one in the appDelegate?

Jefferson
  • 1,457
  • 2
  • 11
  • 13

11 Answers11

274

You can wrap the switching of the rootViewController in a transition animation block:

[UIView transitionWithView:self.window
                  duration:0.5
                   options:UIViewAnimationOptionTransitionFlipFromLeft
                animations:^{ self.window.rootViewController = newViewController; }
                completion:nil];
Ole Begemann
  • 135,006
  • 31
  • 278
  • 256
  • 5
    hey Ole, I tried this approach, it worked partially, the thing is my app shall only stay in landscape mode, but by doing rootviewcontroller transition, the newly-presented view controller is loaded in portrait at the beginning, and quickly rotate to landscape mode, how to solve that? – Chris Chen Nov 08 '11 at 06:15
  • 4
    I answered Chris Chen's question (hopefully! maybe?) in his separate question here: http://stackoverflow.com/questions/8053832/rootviewcontroller-animation-transition-initial-orientation-is-wrong – Kalle Dec 14 '11 at 13:34
  • 1
    hey i want a push transition in same animation can i achieve that? – BhavikKama Jun 23 '14 at 08:42
  • Note that this will case the rootViewController to be nil for that duration. – Anuj Aug 01 '14 at 04:21
  • 14
    I've noticed some issues with this, namely misplaced elements/lazily loaded elements. For instance, if you don't have a navigation bar on the existing root vc, then animate to a new vc that has one, the animation completes AND THEN the nav bar is added. It looks kinda goofy - any thoughts on why this may be, and what can be done? – anon_dev1234 Oct 02 '14 at 15:21
  • In result newViewController get viewWillAppear/viewDidAppear/... twice. Better don't use this approach. – Fanruten Jul 24 '15 at 13:40
  • 1
    I've found that calling `newViewController.view.layoutIfNeeded()` before the animation block fixes issues with lazily-loaded elements. – Whoa Oct 23 '17 at 19:56
66

I found this and works perfectly:

in your appDelegate:

- (void)changeRootViewController:(UIViewController*)viewController {

    if (!self.window.rootViewController) {
        self.window.rootViewController = viewController;
        return;
    }

    UIView *snapShot = [self.window snapshotViewAfterScreenUpdates:YES];

    [viewController.view addSubview:snapShot];

    self.window.rootViewController = viewController;

    [UIView animateWithDuration:0.5 animations:^{
        snapShot.layer.opacity = 0;
        snapShot.layer.transform = CATransform3DMakeScale(1.5, 1.5, 1.5);
    } completion:^(BOOL finished) {
        [snapShot removeFromSuperview];
    }];
}

in your app

 if (!app) { app = (AppDelegate *)[[UIApplication sharedApplication] delegate]; }
        [app changeRootViewController:newViewController];

credits:

https://gist.github.com/gimenete/53704124583b5df3b407

Jesus
  • 8,456
  • 4
  • 28
  • 40
  • Does this support auto screen rotate? – Wingzero Oct 21 '15 at 09:32
  • 1
    This solution worked better in my case. When using transitionWithView the new root view controller was properly laid out until after the transition completed. This approach allows the new root view controller to be added to the window, laid out and then transitioned. – Fostah Mar 04 '17 at 13:22
  • @Wingzero a little bit late, but, this would allow any sort of transition, either via UIView.animations (say, a CGAffineTransform with rotate) or a custom CAAnimation. – Can Apr 11 '17 at 19:28
  • Jesus, it is a bless – Neil Galiaskarov Jul 23 '21 at 04:51
41

I am posting Jesus answer implemented in swift. It takes viewcontroller's identifier as an argument, loads from storyboard desiredViewController and changes rootViewController with animation.

Swift 3.0 Update:

  func changeRootViewController(with identifier:String!) {
    let storyboard = self.window?.rootViewController?.storyboard
    let desiredViewController = storyboard?.instantiateViewController(withIdentifier: identifier);

    let snapshot:UIView = (self.window?.snapshotView(afterScreenUpdates: true))!
    desiredViewController?.view.addSubview(snapshot);

    self.window?.rootViewController = desiredViewController;

    UIView.animate(withDuration: 0.3, animations: {() in
      snapshot.layer.opacity = 0;
      snapshot.layer.transform = CATransform3DMakeScale(1.5, 1.5, 1.5);
      }, completion: {
        (value: Bool) in
        snapshot.removeFromSuperview();
    });
  }

Swift 2.2 Update:

  func changeRootViewControllerWithIdentifier(identifier:String!) {
    let storyboard = self.window?.rootViewController?.storyboard
    let desiredViewController = storyboard?.instantiateViewControllerWithIdentifier(identifier);

    let snapshot:UIView = (self.window?.snapshotViewAfterScreenUpdates(true))!
    desiredViewController?.view.addSubview(snapshot);

    self.window?.rootViewController = desiredViewController;

    UIView.animateWithDuration(0.3, animations: {() in
      snapshot.layer.opacity = 0;
      snapshot.layer.transform = CATransform3DMakeScale(1.5, 1.5, 1.5);
      }, completion: {
        (value: Bool) in
        snapshot.removeFromSuperview();
    });
  }

  class func sharedAppDelegate() -> AppDelegate? {
    return UIApplication.sharedApplication().delegate as? AppDelegate;
  }

After, you have a very simple usage from anywhere:

let appDelegate = AppDelegate.sharedAppDelegate()
appDelegate?.changeRootViewControllerWithIdentifier("YourViewControllerID")

Swift 3.0 update

let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.changeRootViewController(with: "listenViewController")
vkoskiv
  • 65
  • 6
Neil Galiaskarov
  • 5,015
  • 2
  • 27
  • 46
27

Swift 2

UIView.transitionWithView(self.window!, duration: 0.5, options: UIViewAnimationOptions.TransitionFlipFromLeft, animations: {
  self.window?.rootViewController = anyViewController
}, completion: nil)

Swift 3, 4, 5

UIView.transition(with: self.window!, duration: 0.5, options: UIView.AnimationOptions.transitionFlipFromLeft, animations: {
  self.window?.rootViewController = anyViewController
}, completion: nil)
Karen Hovhannisyan
  • 1,140
  • 2
  • 21
  • 31
  • XCode fixed my code like this: ``` UIView.transition(with: self.view.window!, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromTop, animations: { appDelegate.window?.rootViewController = myViewController }, completion: nil) ``` – scaryguy Aug 05 '17 at 01:38
10

just try this. Works fine for me.

BOOL oldState = [UIView areAnimationsEnabled];
[UIView setAnimationsEnabled:NO];
self.window.rootViewController = viewController;
[UIView transitionWithView:self.window duration:0.5 options:transition animations:^{
    //
} completion:^(BOOL finished) {
    [UIView setAnimationsEnabled:oldState];
}];

EDIT:

This one is better.

- (void)setRootViewController:(UIViewController *)viewController
               withTransition:(UIViewAnimationOptions)transition
                   completion:(void (^)(BOOL finished))completion {
    UIViewController *oldViewController = self.window.rootViewController;
    [UIView transitionFromView:oldViewController.view 
                        toView:viewController.view
                      duration:0.5f
                       options:(UIViewAnimationOptions)(transition|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionLayoutSubviews)
                    completion:^(BOOL finished) {
        self.window.rootViewController = viewController;
        if (completion) {
            completion(finished);
        }
    }];
}
Dmitrii Cooler
  • 4,032
  • 1
  • 22
  • 21
  • I had a weird default animation when just simply switching root VC. The first version got rid of that for me. – juhan_h Mar 03 '16 at 23:30
  • Second version will animate the subview layout, as mentioned by juhan_h. If this is not required, either experiment with removing `UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionLayoutSubviews`, or use the first version, or some other method. – ftvs Mar 10 '16 at 10:53
3

In order not to have problems with transition flip later on in the app is good to clear the old view from the stack as well

UIViewController *oldController=self.window.rootViewController;

[UIView transitionWithView:self.window
                  duration:0.5
                   options:UIViewAnimationOptionTransitionCrossDissolve
                animations:^{ self.window.rootViewController = nav; }
                completion:^(BOOL finished) {
                    if(oldController!=nil)
                        [oldController.view removeFromSuperview];
                }];
Catalin
  • 1,821
  • 4
  • 26
  • 32
2

The correct answer is you don't need to replace the rootViewController on your window. Instead, create a custom UIViewController, assign it once and let it display one child controller at a time and replace it with animation if needed. You can use the following piece of code as a starting point:

Swift 3.0

import Foundation
import UIKit

/// Displays a single child controller at a time.
/// Replaces the current child controller optionally with animation.
class FrameViewController: UIViewController {

    private(set) var displayedViewController: UIViewController?

    func display(_ viewController: UIViewController, animated: Bool = false) {

        addChildViewController(viewController)

        let oldViewController = displayedViewController

        view.addSubview(viewController.view)
        viewController.view.layoutIfNeeded()

        let finishDisplay: (Bool) -> Void = {
            [weak self] finished in
            if !finished { return }
            oldViewController?.view.removeFromSuperview()
            oldViewController?.removeFromParentViewController()
            viewController.didMove(toParentViewController: self)
        }

        if (animated) {
            viewController.view.alpha = 0
            UIView.animate(
                withDuration: 0.5,
                animations: { viewController.view.alpha = 1; oldViewController?.view.alpha = 0 },
                completion: finishDisplay
            )
        }
        else {
            finishDisplay(true)
        }

        displayedViewController = viewController
    }

    override var preferredStatusBarStyle: UIStatusBarStyle {
        return displayedViewController?.preferredStatusBarStyle ?? .default
    }
}

And the way you use it is:

...
let rootController = FrameViewController()
rootController.display(UINavigationController(rootViewController: MyController()))
window.rootViewController = rootController
window.makeKeyAndVisible()
...

The example above demonstrates that you can nest UINavigationController inside FrameViewController and that works just fine. This approach gives you high level of customization and control. Just call FrameViewController.display(_) any time you would want to replace the root controller on your window, and it will do that job for you.

Aleks N.
  • 6,051
  • 3
  • 42
  • 42
2

This is an update for swift 3, this method should be in your app delegate, and you call it from any view controller, via a shared instance of the app delegate

func logOutAnimation() {
    let storyBoard = UIStoryboard.init(name: "SignIn", bundle: nil)
    let viewController = storyBoard.instantiateViewController(withIdentifier: "signInVC")
    UIView.transition(with: self.window!, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromLeft, animations: {
        self.window?.rootViewController = viewController
        self.window?.makeKeyAndVisible()
    }, completion: nil)
}

The part that is missing from various questions above, is

    self.window?.makeKeyAndVisible()

Hope this helps someone.

1

in AppDelegate.h:

#define ApplicationDelegate ((AppDelegate *)[UIApplication sharedApplication].delegate)]

in your Controller:

[UIView transitionWithView:self.window
                  duration:0.5
                   options:UIViewAnimationOptionTransitionFlipFromLeft
                animations:^{
    ApplicationDelegate.window.rootViewController = newViewController;
    }
                completion:nil];
nevan king
  • 112,709
  • 45
  • 203
  • 241
Bob
  • 1,351
  • 11
  • 28
  • 6
    This is the same as the accepted answer, except the formatting is wrong. Why bother? – jrturton Jan 17 '13 at 07:22
  • 1
    This one doesn't depend on you being in a View or ViewController. The biggest difference is more philosophical in terms of how thick or thin you like your Views and ViewControllers to be. – Max Jan 19 '14 at 07:54
0

I propose my way which it is working fine in my project, and it offers me good animations. I have tested other proposals found in this post, but some of them do not work as expected.

- (void)transitionToViewController:(UIViewController *)viewController withTransition:(UIViewAnimationOptions)transition completion:(void (^)(BOOL finished))completion {
// Reset new RootViewController to be sure that it have not presented any controllers
[viewController dismissViewControllerAnimated:NO completion:nil];

[UIView transitionWithView:self.window
                  duration:0.5f
                   options:transition
                animations:^{
                    for (UIView *view in self.window.subviews) {
                        [view removeFromSuperview];
                    }
                    [self.window addSubview:viewController.view];

                    self.window.rootViewController = viewController;
                } completion:completion];
}
93sauu
  • 3,770
  • 3
  • 27
  • 43
0

Nice sweet animation (tested with Swift 4.x):

extension AppDelegate {
   public func present(viewController: UIViewController) {
        guard let window = window else { return }
        UIView.transition(with: window, duration: 0.5, options: .transitionFlipFromLeft, animations: {
            window.rootViewController = viewController
        }, completion: nil)
    }
}

Call with

guard let delegate = UIApplication.shared.delegate as? AppDelegate else { return }
delegate.present(viewController: UIViewController())
Cesare
  • 9,139
  • 16
  • 78
  • 130