更改 iOS 窗口的根视图控制器

Posted

技术标签:

【中文标题】更改 iOS 窗口的根视图控制器【英文标题】:Changing root view controller of a iOS Window 【发布时间】:2013-04-02 20:46:03 【问题描述】:

ios Window 的根视图控制器通常在开始时初始化一次为标签栏控制器或导航控制器吗?可以在应用内多次更改根视图控制器吗?

我有一个场景,顶视图根据用户操作而有所不同。我正在考虑拥有一个导航控制器,其顶部视图控制器具有启动屏幕的图像,并根据需要推送/弹出视图控制器。或者,我可以不断更改窗口的顶视图控制器。哪种方法更好?

【问题讨论】:

你能更精确一点吗?根据用户交互更改哪个 ViewController? 你好!看起来你的大部分问题都得到了回答,我在下面包含了一个关于多次设置“rootViewController”的问题。希望这会有所帮助。 【参考方案1】:

iOS 8.0、Xcode 6.0.1、ARC 已启用

您的大部分问题都已得到解答。不过,我可以解决我最近不得不自己处理的一个问题。

可以在应用内多次更改根视图控制器吗?

答案是是的。我最近必须这样做才能在作为应用程序一部分的初始 UIView 之后重置我的 UIView 层次结构。不再需要启动。换句话说,您可以在应用程序之后的任何时间从任何其他 UIViewController 重置您的“rootViewController”。 “didFinishLoadingWithOptions”。

为此...

1) 声明对您的应用的引用。委托(名为“Test”的应用)...

TestAppDelegate *testAppDelegate = (TestAppDelegate *)[UIApplication sharedApplication].delegate;

2) 选择一个你想要的 UIViewController 作为你的“rootViewController”;从情节提要或以编程方式定义...

    a) 故事板(确保标识符,即storyboardID,存在 在 UIViewController 的身份检查器中):
UIStoryboard *mainStoryBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];

NewRootViewController *newRootViewController = [mainStoryBoard instantiateViewControllerWithIdentifier:@"NewRootViewController"];

    b) 以编程方式(可以添加Subview 等)
UIViewController *newRootViewController = [[UIViewController alloc] init];
newRootViewController.view = [[UIView alloc] initWithFrame:CGRectMake(0, 50, 320, 430)];
newRootViewController.view.backgroundColor = [UIColor whiteColor];

3) 将它们放在一起...

 testAppDelegate.window.rootViewController = newRootViewController;
[testAppDelegate.window makeKeyAndVisible];

4) 你甚至可以加入动画...

testAppDelegate.window.rootViewController = newRootViewController;
    [testAppDelegate.window makeKeyAndVisible];

newRootViewController.view.alpha = 0.0;

    [UIView animateWithDuration:2.0 animations:^

        newRootViewController.view.alpha = 1.0;

    ];

希望这对某人有所帮助!干杯。

窗口的根视图控制器。

根视图控制器提供窗口的内容视图。 将视图控制器分配给此属性(以编程方式 或使用 Interface Builder)将视图控制器的视图安装为 窗口的内容视图。如果窗口具有现有视图 层次结构,旧视图在新视图被删除之前被删除 安装。该属性的默认值为 nil。

*2015 年 9 月 2 日更新

正如下面的 cmets 所指出的,当新的视图控制器出现时,您必须处理旧视图控制器的移除。你可以选择有一个过渡视图控制器来处理这个问题。以下是有关如何实现此功能的一些提示:

[UIView transitionWithView:self.containerView
                  duration:0.50
                   options:options
                animations:^

                    //Transition of the two views
                    [self.viewController.view removeFromSuperview];
                    [self.containerView addSubview:aViewController.view];

                
                completion:^(BOOL finished)

                    //At completion set the new view controller.
                    self.viewController = aViewController;

                ];

【讨论】:

像这样替换 rootViewController 时要非常小心,因为如果 rootViewController 以模态方式呈现另一个 viewcontroller,即使你替换了 rootViewController(除非你先关闭它),这个 viewcontroller 仍将保留在窗口层次结构中。 嘿 ale84,我们有解决这个问题的方法吗? @Sahil Kapoor - 我从来没有发现过这种情况,***.com/a/5130560/4018041,当窗口退出 keyWindowStatus 时,会自动调用 -(void)resignKeyWindow;这在您将 -makeKeyAndVisible 发送到 *.window 后被重置。但是,这可能适用于 UIViews,并且可能在 iOS7.0+ 之前也存在。其实有些文章提到的*.windows数组我是无法访问的。 当模态呈现的视图控制器尝试更改窗口的根视图控制器时会发生这种情况。如果您有透明的导航栏,您可能会注意到。解决方法是在动画设置为 false 的情况下调用解除它并在其完成块中更改根视图控制器。 ' self.dismissViewControllerAnimated(false, completion: () -> Void in // Change Root vc here ) ' @Sahil Kapoor - 是的,很好的解决方法。像这样替换视图控制器会导致两个视图控制器相互叠放,但这仅限于两个视图控制器相互叠放,一旦出现另一个视图控制器,三个视图控制器中的最后一个将被关闭自动(至少这是我从测试中看到的)。我认为您绝对可以在不再需要视图控制器后将其关闭,即在动画完成呈现新视图控制器后,这是一种更好的做法。【参考方案2】:

使用“呈现的视图控制器”(presentViewController:animated:completion:)更为常见。您可以拥有任意数量的这些,有效地出现在(并且基本上替换)根视图控制器的前面。如果您不想要,则不必有任何动画,或者可以有。您可以关闭呈现的视图控制器以返回原始根视图控制器,但您不必这样做;如果您愿意,呈现的视图控制器可以永远存在。

这是我书中关于呈现视图控制器的部分:

http://www.apeth.com/iOSBook/ch19.html#_presented_view_controller

在这张图中(来自该章前面的部分),呈现的视图控制器已经完全接管了应用程序界面;根视图控制器及其子视图不再在界面中。根视图控制器仍然存在,但这是轻量级的,无所谓。

【讨论】:

很好的解释。我从应用程序的根视图控制器模态显示登录屏幕的旧技巧在 iOS 8 中无法完全正常工作(在goo.gl/cr9Pxk 中描述了问题)。我想我会跟随你的脚步,制作两个独立的视图控制器:一个用于登录,一个用于主要功能。 (顺便说一句,对于变化如此之快的技术而言,它必须令人沮丧的重写、重新输入设置、收集新的屏幕截图等……)。【参考方案3】:

根据 serge-k 的回答中的 cmets,我已经构建了一个可行的解决方案,当在旧 rootViewController 上出现模态视图控制器时,该解决方案具有奇怪行为的解决方法:

extension UIView 
    func snapshot() -> UIImage 
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.mainScreen().scale)
        drawViewHierarchyInRect(bounds, afterScreenUpdates: true)
        let result = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return result
    


extension UIWindow 
    func replaceRootViewControllerWith(_ replacementController: UIViewController, animated: Bool, completion: (() -> Void)?) 
        let snapshotImageView = UIImageView(image: self.snapshot())
        self.addSubview(snapshotImageView)

        let dismissCompletion =  () -> Void in // dismiss all modal view controllers
            self.rootViewController = replacementController
            self.bringSubview(toFront: snapshotImageView)
            if animated 
                UIView.animate(withDuration: 0.4, animations:  () -> Void in
                    snapshotImageView.alpha = 0
                , completion:  (success) -> Void in
                    snapshotImageView.removeFromSuperview()
                    completion?()
                )
            
            else 
                snapshotImageView.removeFromSuperview()
                completion?()
            
        
        if self.rootViewController!.presentedViewController != nil 
            self.rootViewController!.dismiss(animated: false, completion: dismissCompletion)
        
        else 
            dismissCompletion()
        
    

要替换 rootViewController 只需使用:

let newRootViewController = self.storyboard!.instantiateViewControllerWithIdentifier("BlackViewController")
UIApplication.sharedApplication().keyWindow!.replaceRootViewControllerWith(newRootViewController, animated: true, completion: nil)

希望这会有所帮助:) 在 iOS 8.4 上测试;还测试了导航控制器支持(应该也支持标签栏控制器等,但我没有测试它)

说明

如果有一个模态视图控制器出现在旧 rootViewController 之上,则 rootViewController 会被替换,但旧视图仍然挂在新 rootViewController 的视图下方(例如,可以在水平翻转或交叉溶解过渡动画中看到)和旧的视图控制器层次结构仍处于分配状态(如果多次替换,可能会导致严重的内存问题)。

所以唯一的解决方案是关闭所有模态视图控制器,然后替换 rootViewController。屏幕快照在关闭和替换期间放置在窗口上方,以隐藏丑陋的闪烁过程。

【讨论】:

不错的解决方案,但是如何为TabBarController 解决它? 如果您有多个嵌套的 vcs,这是否有效?比如rootvc>modalvc>modalvc>modalvc>modalvc等 回答自己,我认为确实如此。来自dismiss的文档:“如果您连续呈现多个视图控制器,从而构建一组呈现的视图控制器,则在堆栈中较低的视图控制器上调用此方法会关闭其直接子视图控制器以及该子视图控制器上方的所有视图控制器堆栈。” iOS 13 模拟器崩溃并出现错误Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value【参考方案4】:

您可以在整个应用程序生命周期中更改窗口的 rootViewController。

UIViewController *viewController = [UIViewController alloc] init];
[self.window setRootViewController:viewController];

当您更改 rootViewController 时,您可能仍希望在窗口上添加一个 UIImageView 作为子视图以充当启动图像。我希望这是有道理的,像这样:

- (void) addSplash 
    CGRect rect = [UIScreen mainScreen].bounds;
    UIImageView *splashImage = [[UIImageView alloc] initWithFrame:rect];
    splashImage.image = [UIImage imageNamed:@"splash.png"];
    [self.window addSubview:splashImage];


- (void) removeSplash 
    for (UIView *view in self.window.subviews) 
      if ([view isKindOfClass:[UIImageView class]]) 
        [view removeFromSuperview];
      
    

【讨论】:

我在使用 setRootViewController 时遇到问题。问题是在 rootviewcontroller 替换后视图控制器堆栈仍然保留。不知道那些来自哪里。我调用 storyboard.instantiate 来创建一个新的视图控制器。【参考方案5】:

对于iOS8,我们还需要将下面两个参数设置为YES。

providesPresentationContextTransitionStyle
definesPresentationContext

这是我在 iOS 6 及更高版本的导航控制器下呈现透明模型视图控制器的代码。

ViewController *vcObj = [[ViewController alloc] initWithNibName:NSStringFromClass([ViewController class]) bundle:nil];
UINavigationController *navCon = [[UINavigationController alloc] initWithRootViewController:vcObj];

if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0) 

    navCon.providesPresentationContextTransitionStyle = YES;
    navCon.definesPresentationContext = YES;
    navCon.modalPresentationStyle = UIModalPresentationOverCurrentContext;

    [self presentViewController:navCon animated:NO completion:nil];

else 

    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    [self presentViewController:navCon animated:NO completion:^
        [navCon dismissViewControllerAnimated:NO completion:^
            appDelegate.window.rootViewController.modalPresentationStyle = UIModalPresentationCurrentContext;
            [self presentViewController:navCon animated:NO completion:nil];
            appDelegate.window.rootViewController.modalPresentationStyle = UIModalPresentationFullScreen;

        ];
    ];

【讨论】:

【参考方案6】:

对于尝试为 iOS 13 及更高版本更改根视图控制器的人,您需要使用SceneDelegatewindow 属性更改根视图控制器。

class SceneDelegate: UIResponder, UIWindowSceneDelegate 

  var window: UIWindow?
  static let shared = SceneDelegate()

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) 

    guard let _ = (scene as? UIWindowScene) else  return 

    //other stuff
  

创建了一个具有更改根视图控制器方法的实用程序类。

class AppUtilities 

  class func changeRootVC( _ vc: UIViewController) 

    SceneDelegate.shared.window?.rootViewController = vc
    SceneDelegate.shared.window?.makeKeyAndVisible()
  

您可以通过以下方式更改根视图控制器。

//Here I'm setting HomeVC as root view controller

if let homeVC = UIStoryboard(name: "Main", bundle: nil)?.instantiateViewController(identifier: "HomeVC") as? HomeVC 

    let rootVC = UINavigationController(rootViewController: homeVC)
    AppUtilities.changeRootVC(rootVC)

  

【讨论】:

以上是关于更改 iOS 窗口的根视图控制器的主要内容,如果未能解决你的问题,请参考以下文章

当被模态视图控制器覆盖时,iOS 6 视图控制器布局在方向更改后不会更新

情节提要:更改情节提要中指定的 UINavigation 控制器的根视图

iOS 中常见场景的根视图控制器问题

iOS7和iOS8之间的根视图控制器差异

如何在窗口中添加视图,该视图将在每个视图控制器的滑动时显示?

如何更改导航控制器的根控制器出现在屏幕上的方式?