addChildViewController实际上做了什么?

时间:2013-06-19 13:06:45

标签: ios uiviewcontroller uicontainerview

我刚刚第一次踏入iOS开发阶段,我必须做的第一件事就是实现custom container view controller - 让我们称之为SideBarViewController - 交换它显示的几个可能的子视图控制器中的哪一个,几乎完全像标准标签栏控制器。 (它几乎是一个标签栏控制器,但有一个可隐藏的侧边菜单而不是标签栏。)

根据Apple文档中的说明,每当我将一个子ViewController添加到我的容器时,我都会调用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;
}

...然后,就我所知,我的应用程序仍能完美运行!

Apple文档并未详细说明addChildViewController的作用,或者为什么我们应该称之为{{1}}。目前在UIViewController Class Reference的部分中对方法的作用或原因应该使用的相关描述的整个范围是:

  

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

早些时候在同一页上也有这一段:

  

在将子视图的根视图添加到视图层次结构之前,容器视图控制器必须将子视图控制器与其自身关联。这允许iOS正确地将事件路由到子视图控制器以及这些控制器管理的视图。同样,在从视图层次结构中删除子视图的根视图后,它应该断开该子视图控制器与其自身的连接。要建立或断开这些关联,容器会调用基类定义的特定方法。这些方法不是由容器类的客户端调用的;它们只能由容器的实现来使用,以提供预期的包含行为。

     

以下是您可能需要致电的基本方法:

     

addChildViewController:
  removeFromParentViewController
  willMoveToParentViewController:
  didMoveToParentViewController:

但它没有提供任何关于它所谈论的“事件”或“预期遏制行为”的线索,或者为什么(甚至何时)调用这些方法是“必不可少的”。

Apple文档的“自定义容器视图控制器”部分中的自定义容器视图控制器的示例都调用此方法,因此我认为除了将子ViewController弹出到阵列之外,它还有一些重要的用途,但我可以弄清楚那个目的是什么。这个方法做了什么,为什么要调用它?

4 个答案:

答案 0 :(得分:107)

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

我正在开发一个图书馆应用程序,并希望显示一个很好的记事本视图,当用户想要添加记事时会显示该视图。

enter image description here

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

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

enter image description here

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

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

在过去,在引入子视图控制器的概念之前。但现在,您只需addChildViewController到主视图控制器,一切都将按预期工作,无需任何手动工作。

修改 转发到子视图控制器的事件有两类:

1-外观方法:

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

2-轮换方法:

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

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

答案 1 :(得分:91)

我也想知道这个问题。我观看了Session 102 of the WWDC 2011个视频,View Controller先生Bruce D. Nilo说了这个:

  

viewWillAppear:viewDidAppear:等与addChildViewController:无关。所有addChildViewController:所做的就是说“这个视图控制器是那个视图控制器的孩子”,它与视图外观无关。被调用时与视图进出窗口层次结构相关联。

所以似乎对addChildViewController:的调用很少。呼叫的副作用是重要的部分。它们来自parentViewControllerchildViewControllers关系。以下是我所知道的一些副作用:

  • 将外观方法转发给子视图控制器
  • 转发轮换方法
  • (可能)转发内存警告
  • 避免不一致的VC层次结构,尤其是在transitionFromViewController:toViewController:…中,两个VC需要拥有相同的父级
  • 允许自定义容器视图控制器参与状态保存和恢复
  • 参与响应者链
  • 连接navigationControllertabBarController等属性

答案 2 :(得分:9)

-[UIViewController addChildViewController:]只在视图控制器数组中添加传递的视图控制器,而viewController(父级)希望保留对它的引用。实际上,您应该将这些viewController视图添加为另一个视图的子视图(例如parentViewController的视图),从而自己在屏幕上添加这些视图。 Interface Builder中还有一个便利对象,可以在Storyboard中使用childrenViewControllers。

以前,为了保留您使用其视图的其他viewControllers的引用,您必须在@properties中保留它们的手动引用。拥有像childViewControllersparentViewController之类的内置属性是管理此类交互和构建组合viewControllers的便捷方式,例如您在iPad应用上找到的UISplitViewController。

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

那就是它。

答案 3 :(得分:0)

在进行视图控制器包含时,您需要通过执行 willMoveToParentViewControllerdidMoveToParentViewController 来保持视图层次结构与视图控制器层次结构同步:

- (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 the documentation 所说:

<块引用>

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

而且,如果您想知道为什么我们不在这种情况下也调用 addChildViewController:,那是因为,正如 the documentation 所说,willMoveToParentViewController 会为您这样做: <块引用>

当您的自定义容器调用 addChildViewController 方法时,它会在添加之前自动调用要添加为子项的视图控制器的 addChildViewController: 方法。

同样,在删除时,您调用 willMoveToParentViewController:willMoveToParentViewControllerremoveFromSuperview。正如 removeFromParentViewControllerthe documentation 所说:

<块引用>

如果您要实现自己的容器视图控制器,则必须先调用子视图控制器的 willMoveToParentViewController 方法,然后再调用 willMoveToParentViewController: 方法,传入 removeFromParentViewController 的父值。

再说一次,如果您想知道为什么我们在移除子项时不调用 nil,那是因为,正如 the documentation 所说,didMoveToParentViewController 会为您这样做:< /p> <块引用>

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

仅供参考,如果对子视图的移除进行动画处理,请在动画完成处理程序中调用 didMoveToParentViewController:

但是,如果您按照上面概述的正确顺序执行遏制调用,则子级将收到所有与视图相关的适当事件。

有关详细信息(特别是为什么这些 removeFromParentViewControllerwillMoveToParentViewController 调用如此重要),请参阅 WWDC 2011 视频 Implementing UIViewController Containment。另请参阅didMoveToParentViewController documentation实现容器视图控制器部分。


作为一个小观察,请确保在将子视图添加为子视图时,引用父视图控制器视图的 UIViewController,而不是 bounds。父视图的 frame 位于其父视图的坐标系中。 frame 位于其自己的坐标系中。

当父视图占据全屏时,您可能不会注意到差异,但是一旦您在父视图没有占据全屏的情况下使用它,您就会开始遇到帧错位.为儿童设置坐标时始终使用 bounds。 (或者使用约束,让你完全摆脱这种愚蠢。)


也许不用说,如果您只想在实例化父级时添加子级,则可以完全在故事板中进行视图控制器包含,而无需这些 bounds/add 和 {{1 }}/remove 调用。只需使用“容器视图”并在初始化期间使用 willMove 传递孩子需要的任何数据。

enter image description here

例如,如果父级有一个名为 didMove 的属性,而您想要更新子级中名为 prepareForSegue 的属性:

bar

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