根视图控制器切换过渡动画

127

在appDelegate中用一个新的视图控制器替换旧的根视图控制器时,是否有一种可以实现过渡/动画效果的方法?

11个回答

274

您可以将rootViewController的切换包装在一个转换动画块中:

[UIView transitionWithView:self.window
                  duration:0.5
                   options:UIViewAnimationOptionTransitionFlipFromLeft
                animations:^{ self.window.rootViewController = newViewController; }
                completion:nil];

5
嘿,Ole,我尝试了这种方法,它部分地起作用了。问题是我的应用程序只能保持横屏模式,但通过进行rootviewcontroller过渡,新呈现的视图控制器在开始时以纵向加载,然后迅速旋转到横屏模式,如何解决这个问题? - Chris Chen
4
我会尽力回答Chris Chen在这里提出的问题:https://dev59.com/GGsz5IYBdhLWcg3wNVAR。 - Kalle
1
嘿,我想要在同一动画中实现推动过渡,我能做到吗? - BhavikKama
14
我注意到这个问题,主要是元素错位或者懒加载。比如说,如果你在现有的根视图控制器上没有导航栏,然后动画切换到一个有导航栏的新视图控制器,动画完成之后才添加导航栏。这看起来有点滑稽 - 你认为可能是什么原因,以及可以做些什么呢? - anon_dev1234
1
我发现在动画块之前调用 newViewController.view.layoutIfNeeded() 可以修复懒加载元素的问题。 - Whoa
显示剩余2条评论

66

我发现这个很完美:

在你的 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];
    }];
}

在你的应用程序中

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

鸣谢:

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


这个支持自动屏幕旋转吗? - Wingzero
1
这个解决方案在我的情况下效果更好。使用transitionWithView时,新的根视图控制器在转换完成之前被正确地布局。这种方法允许将新的根视图控制器添加到窗口中,进行布局,然后再进行转换。 - Fostah
@Wingzero 有点晚了,但是这将允许任何类型的过渡,无论是通过UIView.animations(例如旋转的CGAffineTransform)还是自定义CAAnimation。 - Can
耶稣,这是一种祝福。 - Neil Galiaskarov

42

我正在发布耶稣的回答,这是用Swift实现的。它将viewcontroller的标识符作为参数,从storyboard中加载所需的ViewController,并通过动画更改rootViewController。

Swift 3.0 更新:

  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 更新:

  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;
  }

接下来,您可以从任何地方非常简单地使用:

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

Swift 3.0 更新

let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.changeRootViewController(with: "listenViewController")

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)

XCode 通过以下方式修复了我的代码:UIView.transition(with: self.view.window!, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromTop, animations: { appDelegate.window?.rootViewController = myViewController }, completion: nil) - scaryguy

10

只需尝试一下。对我来说效果很好。

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];
}];

编辑:

这个更好。

- (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);
        }
    }];
}

当我简单地切换根视图控制器时,出现了奇怪的默认动画。第一个版本为我解决了这个问题。 - juhan_h
第二个版本将会按照 juhan_h 的建议对子视图布局进行动画处理。如果不需要此功能,可以尝试移除 UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionLayoutSubviews,或者使用第一个版本或其他方法。 - ftvs

3
为了以后在应用程序中不出现翻转过渡的问题,最好也清除堆栈中的旧视图。
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];
                }];

2
正确的答案是您无需替换窗口上的rootViewController。相反,创建一个自定义的UIViewController,仅分配一次,并让它一次显示一个子控制器,并在需要时用动画替换它。您可以使用以下代码作为起点:

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
    }
}

你使用它的方式是:

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

上面的示例演示了您可以将UINavigationController嵌套在FrameViewController中,而且这样做非常好。这种方法为您提供了高度的自定义和控制。只需随时调用FrameViewController.display(_),您就可以更换窗口上的根控制器,它会为您完成这项工作。


2

这是针对Swift 3的更新,此方法应该在您的应用程序委托中,您可以通过应用程序委托的共享实例从任何视图控制器调用它。

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)
}

上述各个问题中缺少的部分是:

    self.window?.makeKeyAndVisible()

希望这能帮到某些人。

1

在AppDelegate.h文件中:

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

在你的控制器中:

[UIView transitionWithView:self.window
                  duration:0.5
                   options:UIViewAnimationOptionTransitionFlipFromLeft
                animations:^{
    ApplicationDelegate.window.rootViewController = newViewController;
    }
                completion:nil];

6
除了格式有误之外,这与被接受的答案是一样的。为什么要费心呢? - jrturton
1
这个不依赖于你是否在视图或视图控制器中。最大的区别在于哲学上更注重你喜欢视图和视图控制器的厚度或薄度。 - Max

0

我提出了我的方法,在我的项目中它运行良好,并且为我提供了良好的动画效果。我已经测试了在这篇文章中发现的其他建议,但其中一些并没有按预期工作。

- (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];
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接