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

70

iOS窗口的根视图控制器通常在一开始就初始化为选项卡控制器或导航控制器吗?在应用程序中多次更改根视图控制器是否可以?

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


你能更具体一些吗?是哪个ViewController根据用户交互而改变了? - Ben-G
嘿!看起来你的大部分问题都得到了解答,我在下面附上了一个关于多次设置“rootViewController”的问题。希望这能有所帮助。 - serge-k
6个回答

52

iOS 8.0, Xcode 6.0.1,启用了ARC

你的大多数问题已经得到解答。但是,我可以处理最近自己遇到的一个问题。

在应用程序中更改根视图控制器多次是否可行?

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

操作步骤...

1)声明对应用程序委托的引用(名为"Test"的应用程序)...

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

2) 选择一个你想要作为“rootViewController”的UIViewController,可以从storyboard中选择,也可以通过编程定义...

    a) storyboard (请确保在UIViewController的Identity Inspector中存在标识符,即storyboardID):

UIStoryboard *mainStoryBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];

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

    b) programmatically (could addSubview, etc.)

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;

    }];

希望这能帮助到有需要的人!祝好!
窗口的根视图控制器。
根视图控制器提供窗口的内容视图。将视图控制器分配给此属性(无论是通过编程还是使用界面生成器)会将视图控制器的视图安装为窗口的内容视图。如果窗口具有现有的视图层次结构,则在安装新视图之前,旧视图将被删除。此属性的默认值为nil。
*更新9/2/2015
如下方评论所指出,当呈现新的视图控制器时,您必须处理旧视图控制器的移除。您可以选择拥有一个过渡的视图控制器来处理此问题。以下是一些实现此操作的提示:
[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;

                }];

34
替换 rootViewController 时要非常小心,因为如果当前的 rootViewController 正在以模态方式呈现另一个视图控制器,那么即使您替换了 rootViewController(除非您先将其解除),该视图控制器仍将保留在窗口层次结构中。 - ale84
嘿,ale84,我们有没有针对这个问题的解决方法? - Sahil Kapoor
1
@Sahil Kapoor - 我从未发现过这种情况,stackoverflow.com/a/5130560/4018041,当窗口放弃keyWindowStatus时,-(void)resignKeyWindow会自动调用;在你发送-makeKeyAndVisible到*.window之后,这将被重置。然而,这可能对UIView是正确的,并且可能在iOS7.0+之前就存在了。实际上,我无法访问一些文章所提到的*.windows数组。 - serge-k
4
当以模态方式呈现的视图控制器试图更改窗口的根视图控制器时,就会发生这种情况。如果您有透明的导航栏,您可能会注意到这一点。 解决方法是在动画设置为false的情况下调用dismiss,并在其完成块中更改根视图控制器。' self.dismiss(animated: false, completion: { () -> Void in // 在此更改根vc }) ' - Sahil Kapoor
2
@Sahil Kapoor - 是的,这是一个不错的解决方案。像这样替换视图控制器将导致两个视图控制器重叠在一起,但这仅限于两个视图控制器重叠在一起,一旦另一个视图控制器被呈现,三个中的最后一个将自动关闭(至少这是我从我的测试中看到的)。我认为你肯定可以在不再需要它的时候关闭视图控制器,即在动画完成呈现新视图控制器之后,这将是一个更好的做法。 - serge-k
显示剩余3条评论

48

通常使用“已呈现视图控制器”(presentViewController:animated:completion:)。您可以有任意数量的这些视图控制器,它们有效地出现在根视图控制器的前面(并基本上被替换)。如果您不想要动画效果,则不必使用动画。您可以关闭已呈现的视图控制器以返回到原始的根视图控制器,但您不必这样做;如果您愿意,已呈现的视图控制器可以一直存在。

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

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

在该章节早期的这个图表中,一个已呈现的视图控制器已经完全接管了应用程序界面;根视图控制器及其子视图不再在界面中。根视图控制器仍然存在,但它是轻量级的,无关紧要。

enter image description here


2
非常好的解释。我的旧技巧是从我的应用程序根视图控制器模态呈现登录屏幕,在iOS 8中并不完全正确(在http://goo.gl/cr9Pxk中描述了问题)。我想我会跟随您的步伐,创建两个独立的视图控制器:一个用于登录,另一个用于主要功能。(顺便说一句,为了适应变化如此之快的技术,重新编写、重新设置、收集新的屏幕截图等等肯定是很令人沮丧的事情)。 - wcochran

40

从serge-k的回答评论中,我已经创建了一个可行的解决方案,并解决了旧的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上进行了测试;也测试了对导航控制器的支持(应该也支持选项卡控制器等,但我没有测试)

说明

如果一个模态视图控制器出现在旧的根视图控制器之上,根视图控制器会被替换,但旧视图仍然悬挂在新的根视图控制器视图下面(例如在水平翻转或淡入淡出的过渡动画期间可见),并且旧的视图控制器层次仍被分配(如果替换发生多次则可能会导致严重的内存问题)。

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


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

6
您可以在应用程序生命周期内更改窗口的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来创建一个新的视图控制器。 - Bagusflyer

3

对于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;

        }];
    }];
}

3

对于那些尝试在 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)

  }
}

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