addChildViewController实际上是做什么的?

105
我第一次接触iOS开发,需要实现一个自定义容器视图控制器custom container view controller,我们称之为SideBarViewController,它可以交换显示多个可能的子视图控制器,就像标准的Tab Bar Controller一样。 (它基本上是一个带有可隐藏侧边菜单而不是选项卡栏的Tab Bar Controller。)
按照Apple文档中的说明,每当我向容器添加一个子视图控制器时,都会调用addChildViewController。我的代码用于交换当前由SideBarViewController显示的子视图控制器如下:
- (void)showViewController:(UIViewController *)newViewController {
    UIViewController* oldViewController = [self.childViewControllers 
                                           objectAtIndex:0];
    
    [oldViewController removeFromParentViewController];
    [oldViewController.view removeFromSuperview];
    
    newViewController.view.frame = CGRectMake(
        0, 0, self.view.frame.size.width, self.view.frame.size.height
    );
    [self addChildViewController: newViewController];
    [self.view addSubview: newViewController.view];
}

然后我开始尝试弄清楚这里的addChildViewController到底是做什么的,但我意识到我不知道。除了将新的ViewController添加到.childViewControllers数组中,它似乎对任何事情都没有影响。即使我从未调用addChildViewController,从子控制器的视图到故事板上设置的子控制器的操作和插座仍然可以正常工作,我无法想象它还可能影响什么。

实际上,如果我重写我的代码而不调用addChildViewController,并且看起来像这样...

- (void)showViewController:(UIViewController *)newViewController {

    // Get the current child from a member variable of `SideBarViewController`
    UIViewController* oldViewController = currentChildViewController;

    [oldViewController.view removeFromSuperview];

    newViewController.view.frame = CGRectMake(
        0, 0, self.view.frame.size.width, self.view.frame.size.height
    );
    [self.view addSubview: newViewController.view];

    currentChildViewController = newViewController;
}

... 那么就目前而言,我的应用程序仍然可以正常工作!

苹果文档并没有详细解释addChildViewController的作用或者我们为什么需要调用它。在UIViewController类参考中,关于这个方法的相关描述如下:

将给定的视图控制器添加为子控制器。 ... 此方法仅适用于自定义容器视图控制器的实现。如果您重写此方法,则必须在您的实现中调用super。

此外,同一页之前有一段描述:

你的容器视图控制器必须在将子视图的根视图添加到视图层次结构之前,将子视图控制器与自身关联。这样iOS才能正确地将事件路由到子视图控制器和这些控制器管理的视图。同样,在从其视图层次结构中删除子视图的根视图后,它应该断开该子视图控制器与自身的联系。为了建立或断开这些关联,您的容器调用基类定义的特定方法。这些方法不是容器类的客户端调用的,它们仅由容器的实现使用以提供预期的包含行为。
以下是您可能需要调用的关键方法: addChildViewController: removeFromParentViewController willMoveToParentViewController: didMoveToParentViewController:
但是,它并没有提供任何线索,解释它所说的“事件”或“预期的包含行为”是什么,也没有解释为什么(甚至何时)调用这些方法是“必要的”。
苹果文档中“自定义容器视图控制器”部分的自定义容器视图控制器示例都调用此方法,因此我假设它除了将子视图控制器弹出数组之外还具有某些重要目的,但我无法确定该目的是什么。这个方法是做什么的,为什么我应该调用它?

3
苹果公司的2011 WWDC视频页面有一个关于这个主题的非常好的会议(“实现UIViewController容器化”)。 - Alladinian
4个回答

113

我认为一个例子胜过千言万语。

我正在开发一个图书馆应用程序,并想展示一个漂亮的记事本视图,当用户想要添加笔记时,该视图会出现。

enter image description here

尝试了一些解决方案后,我最终创造了自己的定制解决方案来显示记事本。 所以当我想显示记事本时,我创建一个新的NotepadViewController实例,并将其根视图添加为主视图的子视图。 到目前为止一切都很好。

然后我注意到,在横向模式下,记事本图像部分隐藏在键盘下面。

enter image description here

所以我想改变记事本图像并将其上移。 为此,我在willAnimateRotationToInterfaceOrientation:duration:方法中编写了正确的代码,但是当我运行应用程序时,什么也没发生! 经过调试,我注意到NotepadViewController中没有任何UIViewController的旋转方法实际被调用。 只有主视图控制器中的这些方法被调用。

为了解决这个问题,我需要在主视图控制器中手动调用NotepadViewController中的所有方法。 这很快会使事情变得复杂,并在应用程序中创建不相关组件之间的额外依赖关系。

那是在引入子视图控制器概念之前的事情。 但现在,您只需将addChildViewController添加到主视图控制器中,一切都将按预期工作,无需任何更多的手动操作。

编辑:有两类事件被转发给子视图控制器:

1- 外观方法:

- viewWillAppear:
- viewDidAppear:
- viewWillDisappear:
- viewDidDisappear:

2- 旋转方法:

- willRotateToInterfaceOrientation:duration:
- willAnimateRotationToInterfaceOrientation:duration:
- didRotateFromInterfaceOrientation:

您还可以通过覆盖 shouldAutomaticallyForwardRotationMethodsshouldAutomaticallyForwardAppearanceMethods 来控制您希望自动转发的事件类别。


从文档和快速测试中得出的结论是,我认为没有任何其他事件只会在将addChildViewController添加到父控制器时转发。 - Hejazi
希望它能自动转发 viewWillLayoutSubviews。 - MobileMon

98
我也对这个问题感到疑惑。我观看了WWDC 2011的102次会议视频,View Controller先生Bruce D. Nilo说道:

viewWillAppear:, viewDidAppear:等与addChildViewController:无关。所有addChildViewController:所做的就是告诉"这个视图控制器是那个视图控制器的子级",并且它与视图外观无关。当它们被调用时,与视图何时进出窗口层次结构相关。

因此,似乎调用addChildViewController:很少起作用。调用的副作用才是重要的部分。它们来自于parentViewControllerchildViewControllers之间的关系。以下是我所知道的一些副作用:
  • 将外观方法转发给子视图控制器
  • 转发旋转方法
  • (可能)转发内存警告
  • 避免不一致的VC层次结构,特别是在transitionFromViewController:toViewController:…中,两个VC都需要有相同的父级
  • 允许自定义容器视图控制器参与状态保存和恢复
  • 参与响应者链
  • 连接navigationControllertabBarController等属性

2
+1 for the responder chain. 如果您想在子视图上接收触摸事件,则需要使用addChildViewController。 - charlieb

10

-[UIViewController addChildViewController:] 只是将传入的视图控制器添加到视图控制器数组中,供视图控制器(父级)保留引用。您应该通过将它们作为另一个视图的子视图添加到屏幕上来实际添加这些视图控制器的视图(例如 parentViewController 的视图)。在 Storyboards 中,还有一个方便的接口构建对象使用 childrenViewControllers。

以前,为了保留您使用其视图的其他视图控制器的引用,您必须在 @properties 中手动引用它们。拥有内置属性,如 childViewControllers 和随之而来的 parentViewController 是一种方便的方式来管理此类交互并构建像 iPad 应用程序上找到的 UISplitViewController 这样的组合视图控制器。

此外,childrenViewControllers 还会自动接收父级接收到的所有系统事件:-viewWillAppear、-viewWillDisappear 等。以前,您应该在 "childrenViewControllers" 上手动调用这些方法。

就是这样。


你认为它只做这些的依据是什么?此外,你能提供一个被子进程接收到的“系统事件”列表吗?在谷歌上搜索“iOS系统事件”并没有找到太多相关信息;苹果好像并没有使用这个术语? - Mark Amery
这基本上是一个方便的方法,允许您将视图控制器B的视图作为视图控制器A的子视图添加,但仍然使视图控制器B管理其视图。为了使其正常工作,您需要确保视图控制器B正在获取系统事件(读取UIViewControllerDelegate回调)。'addChildViewController'会自动连接它们,以节省手动传递所有内容的麻烦。 - Sam Clewlow

0

addChildViewController 实际上是做什么的?

这是视图容器的第一步,通过它我们可以将视图层次结构与视图控制器层次结构保持同步,对于那些子视图已经封装了自己的逻辑在其自己的视图控制器中的情况(以简化父视图控制器、启用具有自己逻辑的可重用子视图等)。

因此,addChildViewController 将子视图控制器添加到 childViewControllers 数组中,该数组跟踪子视图控制器,使它们获取所有视图事件,为您保留对子视图的强引用等。

但请注意,addChildViewController 只是第一步。您还需要调用 didMoveToParentViewController

- (void)showViewController:(UIViewController *)newViewController {
    UIViewController* oldViewController = [self.childViewControllers objectAtIndex:0];

    [oldViewController willMoveToParentViewController:nil];   // tell it you are going to remove the child
    [oldViewController.view removeFromSuperview];             // remove view
    [oldViewController removeFromParentViewController];       // tell it you have removed child; this calls `didMoveToParentViewController` for you

    newViewController.view.frame = self.view.bounds;          // use `bounds`, not `frame`
    newViewController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;  // be explicit regarding resizing mask if setting `frame`

    [self addChildViewController:newViewController];          // tell it that you are going to add a child; this calls `willMoveToParentViewController` for you
    [self.view addSubview:newViewController.view];            // add the view
    [newViewController didMoveToParentViewController:self];   // tell it that you are done
}

此外,请注意调用顺序,调用这些方法的顺序很重要。例如,当添加时,您按以下顺序调用 addChildViewControlleraddSubviewdidMoveToParentViewController。请注意,正如文档中针对 didMoveToParentViewController 所说:

如果您正在实现自己的容器视图控制器,则必须在转换到新控制器完成后或(如果没有转换)在立即调用 addChildViewController: 方法后调用子视图控制器的 didMoveToParentViewController: 方法。

如果您想知道为什么我们在这种情况下不调用 willMoveToParentViewController,原因是,正如文档所述,addChildViewController 已经替你做好了:

当您的自定义容器调用addChildViewController:方法时,它会在添加之前自动调用要添加为子视图控制器的视图控制器的willMoveToParentViewController:方法。
同样地,当移除时,您需要调用willMoveToParentViewControllerremoveFromSuperviewremoveFromParentViewController。正如willMoveToParentViewController文档所述:
如果您正在实现自己的容器视图控制器,那么在调用removeFromParentViewController方法之前,它必须调用子视图控制器的willMoveToParentViewController:方法,并传入一个父视图控制器值为nil的参数。

如果你想知道为什么我们在移除子视图时不调用didMoveToParentViewController方法,那是因为,正如文档所说,removeFromParentViewController方法已经替你完成了:

removeFromParentViewController方法会在移除子视图后自动调用子视图控制器的didMoveToParentViewController:方法。

顺便提一下,如果你要对子视图进行动画移除,可以将removeFromParentViewController方法放在动画完成处理程序中。

但是,如果你按照上面概述的正确容器调用序列,则子视图将接收到所有适当的与视图相关的事件。

更多信息(特别是为什么这些willMoveToParentViewControllerdidMoveToParentViewController调用如此重要),请参见WWDC 2011视频实现UIViewController容器。还请参阅UIViewController文档中的实现容器视图控制器部分。


作为一个小的观察,确保在将子视图添加为子视图时,引用父视图控制器视图的bounds而不是frame。父视图的frame位于其父视图的坐标系中。而bounds则在其自己的坐标系中。
当父视图占据整个屏幕时,您可能不会注意到差异,但是一旦您在父视图没有占据整个屏幕的情况下使用它,您将开始遇到框架不对齐的问题。始终在设置子级坐标时使用bounds。(或者使用约束,这样可以完全摆脱这种傻瓜行为。)
或许不用说,如果你只想在父视图被实例化时添加子视图,那么可以完全使用故事板中的视图控制器容器来完成,而无需进行任何add/removewillMove/didMove调用。只需使用“容器视图”并使用prepareForSegue在初始化期间传递子视图所需的任何数据即可。

enter image description here

例如,如果父级有一个名为bar的属性,而您想要更新子级中名为baz的属性:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.destinationViewController isKindOfClass:[ChildViewController class]]) {
        ChildViewController *destination = segue.destinationViewController;

        destination.baz = self.bar;
    }
}

现在,如果您想以编程方式添加/删除子项,则可以按上述方式使用。但是,故事板“容器视图”可以处理所有视图包含调用,适用于非常少的代码的简单场景。


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