[TOC=5]
* * * * *
>原文链接 :[Implementing a Container View Controller](https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html#//apple_ref/doc/uid/TP40007457-CH11-SW1)
容器视图控制器是将来自多个视图控制器的内容合并到单个用户界面中的一种方法。 容器视图控制器通常用于方便导航,并基于现有内容创建新的用户界面。 UIKit 中容器视图控制器的示例包括 UINavigationController,UITabBarController 和 UISplitViewController,所有这些都可以方便用户在不同的界面之间进行导航。
### 设计一个自定义容器视图控制器
大多数情况下,容器视图控制器都像内容视图控制器一样管理根视图和其他一些内容。 区别在于容器视图控制器从其他视图控制器获取其内容。把获取到的内容嵌入到自己的视图层次结构中, 获取的内容取决于其他视图控制器的视图。 容器视图控制器负责设置嵌入视图的大小和位置,嵌入视图具体内容仍然被所属视图控制器管理。
在设计自己的容器视图控制器时,重点关注容器和被包含的视图控制器之间的关系。视图控制器之间的关系可以帮助您了解它们的内容应该如何显示在屏幕上,以及您的容器如何在内部管理它们。在设计过程中,问自己以下几个问题:
* 容器的功能是什么,它的子项有什么功能?
* 同时显示了多少个子项?
* 兄弟视图控制器之间的关系(如果有的话)是什么?
* 如何从容器中添加或删除子视图控制器?
* 子项的大小和位置会改变吗?在什么条件下这些变化会发生?
* 容器是否提供装饰性或导航相关的视图?(比如导航条)
* 容器和它的子项之间需要什么样的通信方式?除了 UIViewController 定义的标准的事件外,容器是否需要向它的子项传递特定的事件?
* 容器的外观可以以不同的方式配置吗?如果可,怎么改变?
在定义了各个对象的功能之后,容器视图控制器的实现相对简单。 UIKit 的唯一要求是在容器视图控制器和子视图控制器之间建立一个正式的父子关系。父-子关系确保子项接收到相关的系统消息。除此之外,实际上大多数工作都是在包含在视图的布局和管理过程中,这对于每个容器来说都是不同的。可以将视图放置在容器的内容区域的任何位置,任意大小。还可以向视图层次添加自定义视图,以提供装饰或帮助导航。
#### 示例1:UINavigationController
UINavigationController 支持通过分层数据集的导航。 导航界面一次显示一个子视图控制器。 界面顶部的导航栏显示数据层次结构中的当前位置,并显示后退按钮以向后移动一个层级。 数据层次的导航留给了子视图控制器,涉及到列表或按钮的使用。
视图控制器之间的导航由导航控制器及其子节点共同管理。 当用户与子视图控制器 A 的按钮或表格行交互时,子视图控制器 A 要求导航控制器将新的视图控制器 B 推入视图。A 处理 B 的内容的配置,但导航控制器管理过渡动画。 导航控制器还管理导航栏,该导航栏显示用来关闭最上面的视图控制器的后退按钮。
图 5-1 显示了导航控制器及其视图的结构。大多数内容区域由最上面子视图控制器填充,只有一小部分被导航栏占据。
###### 图 5-1 导航界面的结构
![](https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/Art/VCPG_structure-of-navigation-interface_5-1_2x.png)
在紧凑的和常规的环境中,导航控制器一次只显示一个子视图控制器。导航控制器调整它的子空间以适应可用的空间。
#### 示例1:UISplitViewController
UISplitViewController 对象以 **主 - 细节** 布置显示两个视图控制器的内容。 在这种安排中,一个视图控制器(主视图)的内容决定了其他视图控制器显示的细节。 两个视图控制器的可见性是可配置的,但也受当前环境的支配。 在正常的水平环境中,UISplitViewController 可以同时显示两个子视图控制器,或者可以根据需要隐藏主视图控制器。 在紧凑的环境中,UISplitViewController 一次只显示一个视图控制器。
图 5-2 显示了在一个规则的水平环境中的 UISplitViewController 及其视图的结构。 UISplitViewController 本身只有默认的容器视图。 在这个例子中,两个子视图是并排显示的。 子视图的大小是可配置的,主视图的可见性也是可配置的。
###### 图 5-2 UISplitViewController 界面
![](https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/Art/VCPG-split-view-inerface_5-2_2x.png)
### 在 Interface Builder 中配置容器
要在设计时创建父子容器关系,请将容器视图对象添加到故事板场景中,如图5-3所示。 容器视图对象是代表子视图控制器内容的占位符对象。 使用该视图来调整和定位与容器中其他视图相关的子视图。
###### 图 5-3 在 Interface Builder 中添加容器视图
![](https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/Art/container_view_embed_2x.png)
当加载一个或多个容器视图的视图控制器时, Interface Builder 还会加载与这些视图关联的子视图控制器。必须在与父类相同的时间实例化子元素,以便创建适当的父子关系。
如果不使用 Interface Builder 来设置 容器-子容器 关系,那么您必须通过编程方式将每个子节点添加到容器视图控制器来创建这些关系,如[ Adding a Child View Controller to Your Content ](https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html#//apple_ref/doc/uid/TP40007457-CH11-SW13)所描述的那样。
### 实现一个自定义容器视图控制器
要实现容器视图控制器,必须在视图控制器和子视图控制器之间建立关系。在尝试管理视图控制器的视图之前,需要建立这些父子关系。这样做可以让 UIKit 知道你的视图控制器是在管理子节点的大小和位置。可以在 Interface Builder 中创建这些关系,或者以编程方式创建它们。在以编程方式创建父子关系时,作为您的视图控制器设置的一部分,需要显式地添加和删除子视图控制器。
#### 添加子视图控制器
要以编程的方式将子视图控制器合并到容器视图控制器中,通过以下步骤创建相关视图控制器之间的父子关系:
1. 调用容器视图控制器的 addChildViewController: 方法。
这个方法告诉 UIKit 你的容器视图控制器现在正在管理子视图控制器的视图。
2. 将子视图添加到容器的视图层次结构中。
永远记得把设置子节点根视图的大小和位置作为这个过程的一部分。
3. 添加任约束来管理子节点根视图的大小和位置。
4. 调用子视图控制器的 didMoveToParentViewController: 方法。
清单 5-1 展示了一个容器视图控制器如何在其容器中嵌入一个子视图控制器。 建立父子关系后,容器设置其子节点的 frame,并将子视图添加到自己的视图层次结构中。 设置子视图的 frame 大小很重要,能确保视图在容器中正确显示。 在添加视图之后,容器视图控制器调用子视图控制器的 didMoveToParentViewController: 方法,以使子视图控制器有机会对视图所有权的更改做出响应。
###### 清单5-1 添加子视图控制器
~~~
- (void) displayContentController: (UIViewController*) content
{
[self addChildViewController:content];
content.view.frame = [self frameForContentController];
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self];
}
~~~
在前面的例子中,注意只调用了孩子的 didMoveToParentViewController: 方法。 那是因为 addChildViewController: 方法为调用了子视图控制器的 willMoveToParentViewController: 方法。 必须自己调用 didMoveToParentViewController: 方法的原因是,只有在将子视图嵌入容器的视图层次结构后,方法才能被调用。
使用自动布局时,在将子视图添加到容器的视图层次结构后,在容器和子对象之间设置约束。 约束只会影响子视图控制器的根视图的大小和位置。 请勿更改子视图控制器的根视图或者其视图层次结构。
#### 删除子视图控制器
要从容器中删除子视图控制器,请通过执行以下操作来删除视图控制器之间的父子关系:
1. 调用子视图控制器的 willMoveToParentViewController: 方法 ,参数为 nil 。
2. 删除子视图控制器的根视图配置的约束。
3. 从容器的视图层次结构中移除子视图控制器的根视图。
4. 调用子视图控制器的 removeFromParentViewController 方法来结束父子关系。
删除子视图控制器会永久切断父级和子级之间的关系。 只有当您不再需要引用子视图控制器时,才能移除子视图控制器。 例如,当新导航控制器被推入导航堆栈时,导航控制器不会移除其当前的子视图控制器。 只有当它们从堆栈中弹出时才会将其移除。
清单5-2显示了如何从容器中移除子视图控制器。调用子视图控制器的willMoveToParentViewController:方法,参数为 nil;为其提供了为更改做准备的机会。调用 removeFromParentViewController 方法同时会调用 didMoveToParentViewController: 方法,参数为 nil。 将父视图控制器设置为 nil,就可以将子视图从容器中删除。
###### Listing 5-2 删除子视图控制器
~~~
- (void) hideContentController: (UIViewController*) content
{
[content willMoveToParentViewController:nil];
[content.view removeFromSuperview];
[content removeFromParentViewController];
}
~~~
#### 子视图控制器之间的过渡
当想要用动画的形式完成一个子视图控制器替换另一个时,将子视图控制器的添加和删除合并到转换动画过程中。 在动画之前,请确保两个子视图控制器都是容器视图控制器的一部分,但让当前的子节点知道它即将消失。 在动画中,将新的子节点的视图移动到正确位并移除旧的子节点的视图。 动画完成后,完成子视图控制器的移除。
清单5-3展示了如何使用转换动画将一个子视图控制器转换为另一个子视图控制器的示例。在这个例子中,新的视图控制器被以动画方式移动到由现有的子视图控制器所占用的矩形区域中,该控制器被移动到屏幕外。在动画完成之后,completion block 从容器中删除子视图控制器。 在这个例子中,` transitionFromViewController:toViewController:duration:options:animations:completion: `方法会自动更新容器的视图层次,所以你不需要自己添加和移除视图。
###### 清单 5-3 两个子视图控制器之间的转换
~~~
- (void)cycleFromViewController: (UIViewController*) oldVC
toViewController: (UIViewController*) newVC
{
// Prepare the two view controllers for the change.
[oldVC willMoveToParentViewController:nil];
[self addChildViewController:newVC];
// Get the start frame of the new view controller and the end frame
// for the old view controller. Both rectangles are offscreen.
newVC.view.frame = [self newViewStartFrame];
CGRect endFrame = [self oldViewEndFrame];
// Queue up the transition animation.
[self transitionFromViewController: oldVC toViewController: newVC
duration: 0.25 options:0
animations:^{
// Animate the views to their final positions.
newVC.view.frame = oldVC.view.frame;
oldVC.view.frame = endFrame;
}
completion:^(BOOL finished) {
// Remove the old view controller and send the final
// notification to the new view controller.
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
}];
}
~~~
#### 管理子节点的外观更新
在将一个子节点添加到容器后,容器会自动将与外观相关的消息转发给子节点。一般来说是必要的的行为,因为它确保所有事件都被正确地发送。但是,有时候默认的行为可能会以一种对您的容器没有意义的顺序发送这些事件。例如,如果多个子节点同时更改它们的视图状态,那么您可能需要调整这些更改,以便外观回调在同一时间以更合理的顺序发生。
要接管外观回调的责任,请覆盖容器视图控制器中的 `shouldAutomaticallyForwardAppearanceMethods`方法,并返回 NO ,如清单 5-4 所示。 返回 NO 让 UIKit 知道你的容器视图控制器通知其子的外观变化。
###### 清单 5-4 禁用自动转发外观消息
~~~
- (BOOL) shouldAutomaticallyForwardAppearanceMethods
{
return NO;
}
~~~
出现转场时,根据需要调用子节点的 `beginAppearanceTransition:animated:` 或 `endAppearanceTransition `方法。 例如,如果您的容器有一个由 child 属性引用的单个子节点,那么您的容器会将这些消息转发给子项,如清单 5-5 所示。
###### 清单 5-5 转发容器出现或消失的外观消息
~~~
-(void) viewWillAppear:(BOOL)animated {
[self.child beginAppearanceTransition: YES animated: animated];
}
-(void) viewDidAppear:(BOOL)animated {
[self.child endAppearanceTransition];
}
-(void) viewWillDisappear:(BOOL)animated {
[self.child beginAppearanceTransition: NO animated: animated];
}
-(void) viewDidDisappear:(BOOL)animated {
[self.child endAppearanceTransition];
}
~~~
### 构建容器视图控制器的建议
设计、开发和测试一个新的容器视图控制器需要时间。尽管单个功能很简单,但是作为一个整体的控制器是相当复杂的。在实现您自己的容器类时,请考虑以下技巧:
* **只访问子视图控制器的根视图**。容器应该只访问每个子节点的根视图,即子节点的 view 属性返回的视图。它不应该访问任何一个子节点的其他视图。
* **子视图控制器应该对它们的容器有最少的了解**。子视图控制器应该把焦点放在它自己的内容上。如果容器允许它的行为受到一个孩子的影响,那么它应该使用委托设计模式来管理这些交互。
* **优先使用常规视图设计容器**。使用常规视图(而不是来自子视图控制器的视图)可以让您有机会在一个简化的环境中测试布局约束和动画转换。当常规视图按预期工作时,将它们放到您的子视图控制器的视图中。
*
### 将控制权委托给子视图控制器
容器视图控制器可以将其自身外观的某些方面委托给一个或多个子视图控制器。你可以用以下方法来委派控制:
* 让一个子视图控制器确定状态栏的样式。 要将状态栏外观委托给子级,请覆盖容器视图控制器中的 childViewControllerForStatusBarStyle 和 childViewControllerForStatusBarHidden 方法中的一个或两个。
* 让子视图控制器指定自己预设的尺寸。 具有灵活布局的容器可以使用子节点的 preferredContentSize 属性来帮助确定子节点的大小。