容器视图控制器是一种结合多个视图控制器的内容到一个单一的用户界面上的方式。容器视图控制器经常被用来使导航更方便,基于已经存在的内容创建一个新的用户界面类型。例如,在UIKit中的容器视图控制器包括UINavigationcontroller,UITabBarcontroller 和 UISplitViewcontroller,它们都可以使用户界面在不同视图部分之间的切换和导航更加的容易。
设计一个自定义的容器视图控制器
在几乎所有的方面,一个容器视图控制器就像其它任何一个内容视图控制器一样,它管理一个根视图和一些内容。这两者之间的不同点是,容器视图控制器从其它视图控制器那里得到一部分内容,得到的内容局限于其它视图控制器的视图,这些内容内嵌于容器视图控制器自己的视图层次中。容器视图控制器为任何内嵌的视图设置大小和位置,但是原先的视图控制器仍然管理这些视图中的内容。
当设计你自己的容器视图控制器时,应该始终理解container 和 contained 容器视图控制器之间的关系。视图控制器之间的关系可以帮助通知它们的内容应该怎样显示到屏幕上,你的容器应该怎样在内部管理它们。在设计的过程中,应该首先问问你自己如下这些问题:
-你的容器和容器的children 扮演什么样的角色?
-多少children可以被同时显示?
-兄弟视图控制器之间的关系是怎样的?
-如何在容器中添加或者移除子视图控制器?
-children 的大小和位置可以改变吗?什么样的条件下这样的改变会发生?
-容器本身提供decorative 或者navigation-related 视图吗?
-在容器和它的children 之间什么样的交流是是应该提供的?容器除了类UIViewController 中定义的标准事件外,还需要向它的children report指定的事件吗?
-可以用不同的方式类配置容器的显示吗?如果可以,应该怎样配置?
在你定义了各种不同的对象之间的规则后,实现一个容器视图控制器是相当简单的。UIKit 的唯一要求是你需要在容器视图控制器和它的任一子视图控制器之间建立一个正式的parent-child关系。parent-child关系可以确保children 接收到系统发来的相关信息。除此之外,大多数实际的工作发生在布局和管理它所包含的视图期间,每个容器视图控制器在这点上都是不同的。你可以在容器视图控制器内容区域的任何地方放置视图,也可以在该区域内设置视图为任何你想要的大小。你也可以在导航中添加定制的视图到视图层次中,用来提供装饰或者帮助。
Example:Navigation Controller
一个UINavigationController 对象通过一个分层次的数据设置来支持导航。一个导航界面一次呈现一个子视图控制器。界面顶部的导航条展示了在数据层次中的当前位置,还展示了一个后退按钮用来退回到上一层。导航下降到数据层是由子视图控制器决定的,而且可能涉及到tables和buttons的使用。
视图控制器之间的导航是被导航控制器和它的children 共同来管理的。当用户通过一个子视图控制器的按钮或者table 行来进行交互的时候,子视图控制器要求导航控制器推送一个新的视图控制器到视图,子视图控制器负责处理新的视图控制器的内容的配置,导航视图控制器负责管理animations 过渡。导航控制器也负责管理导航条,它显示了一个后退按钮,用来dismiss 最上层的视图控制器。
图5-1展示了一个导航控制器和它的视图之间的结构。绝大多数的内容区域是被最上层的子视图控制器填充的,导航条只占据了很少的一部分。
图5-1导航界面的结构
在compact 和 regular 环境中,导航视图控制器一次仅显示一个子视图控制器。导航视图控制器负责重新调整它的子视图大小来适应有效的屏幕空间。
Example:Split View Controller
UISplitViewController 对象在一个主从排列中展示两个视图控制器的内容,在这个排列中,一个视图控制器(主)决定其它的视图控制器显示到界面上的详细内容。两个视图控制器的可见性都是可以配置的,但是它们都被当前的环境来支配。在一个水平的regular 环境中,split视图控制器可以边挨边的显示两个视图控制器,或者也可以隐藏主视图,在需要的时候再显示它。在一个compact环境中,split视图控制器一次仅显示一个视图控制器。
图5-2展示了在水平regular 环境中,一个split视图控制器和它的视图视图之间的结构。split视图控制器本身默认有它自己的容器视图。在这个例子中,两个视图边挨边的显示,子视图的大小是可以配置的,主视图在图中也是可见的。
图5-2一个split视图界面
在Interface Builder中配置一个视图
为了在设计时创建一个parent-child 容器关系,添加一个容器视图对象到你的storyboard 屏幕上,如图5-3那样。一个容器视图对象是一个占位符对象,它代表了子视图控制器的内容。在容器中使用这个视图来设置根视图和其它有关视图的大小和位置。
图5-3在Interface Builder中添加一个容器视图
当你为一个或者多个容器视图加载一个视图控制器的时候,Interface Builder也会加载和这个视图相关的子视图控制器。子视图和父视图必须被同时初始化,这样一个合适的parent-child关系就可以被建立起来了。
如果你不适用Interface Builder来建立你的parent-child容器关系,你必须用编码的方式通过把每个child添加到容器视图控制器中的方式来创建这种关系,详细描述,in Adding a Child View Controller to Your Content.
实现一个定制的容器视图控制器
为了实现一个容器视图控制器,你必须在视图控制器和它的子视图控制器之间建立关系。在你试着管理视图和它任何子视图控制器之前,建立这种parent-child关系是必须的。这样做的话,可以让UIKit知道你的视图控制器正在管理它的子视图的大小和位置。你可以通过Interface Builder 或者编码的方式来创建这种关系。当你通过编码的方式来创建这种关系时,作为建立视图控制器的一部分,你应该显示的添加和移除子视图控制器。
在你的内容中添加一个子视图控制器
为了以编码的方式包含一个子视图控制器到你的内容中,在相关的视图控制器之间创建一个parent-child关系,可以通过下面这些步骤:
1.调用容器视图控制器的addChildViewController:方法。 这个方法告诉UIKit 你的容器视图控制器正在管理相关的视图的子视图控制器。
2.添加child 的根视图到容器的视图层次结构。 在这个过程中,应该始终记得设置child‘s frame 的大小和位置。
3.添加若干约束来管理child 根视图的大小和位置。
4.调用子视图控制器的didMoveToParentViewController:方法。
Listing5-1展示了一个容器时怎样嵌入一个子视图控制器到它的容器中的。在建立了parent-child关系后,容器设置它的child 的frame ,添加child 的视图到它的视图层次结构中。设置child 的视图frame 大小时非常重要的,可以确保视图在你的容器中正确的显示。在添加了视图之后,容器调用child的 didMoveToParentViewController:方法来给子视图控制器一个响应视图所有权改变的机会。(注意,这里的child 指的是容器视图控制器中的子视图控制器)
Listing5-1 添加一个子视图控制器到容器
- (void) displayContentController: (UIViewController*) content {
[self addChildViewController:content];
content.view.frame = [self frameForContentController];
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self];
}
在上面的这个例子中,注意你仅仅调用了child的didMoveToParentViewController:方法。那是因为addChildViewController:方法为你调用了child 的willMoveToParentViewController:方法。你必须自己调用didMoveToParentViewController:方法的原因是在直到你嵌入child的视图到容器的视图层次结构中之后,这个方法不能被调用。
当使用自动布局时,在添加子视图到你的容器的视图层次结构中之后,在容器和它的child之间建立约束。你的约束应该仅仅影响child 根视图的大小和位置,而不能改变child 视图层次结构中根视图或者是其它任何视图的内容。
移除一个子视图控制器
为了从你的内容中移除一个子视图控制器,通过下面这些步骤来移除视图控制器之间的parent-child关系:
1.调用child 值为nil 的willMoveToParentViewController:方法。
2.移除你在child 的根视图上设置的任何约束。
3.从你的容器的视图层次中移除child 的根视图。
4.在parent-child关系的最后,调用child 的removeFromParentViewController 方法来结束。
移除一个子视图控制器就永久的割断了child 和parent 之间的联系。仅仅当你不在需要引用一个子视图控制器的时候才移除它。例如,当一个新的视图控制器被push 到navigation stack时,导航视图控制器不会移除它当前的子视图控制器。仅仅当子视图控制器被pop 出navigation stack 时才移除它。
Listing 5-2展示了如何移从容器中移除它的子视图控制器。调用参数为nil 的willMoveToParentViewController:方法,给子视图控制器一个为改变做好准备的机会的机会。removeFromParentViewController 方法也调用子视图控制器的didMoveToParentViewController:方法,传递给该方法一个值为nil 的参数。最后设置父视图控制器指向nil 从你的容器中移除子视图控制器的视图。
Listing5-2 从容器中移除一个子视图控制器
- (void) hideContentController: (UIViewController*) content {
[content willMoveToParentViewController:nil];
[content.view removeFromSuperview];
[content removeFromParentViewController];
}
在子视图控制器之间切换
当你想要animate 一个子视图控制器来替换另一个的时候,包含一个新添加的视图控制器到transition animation 过程的同时移除另一个子视图控制器。在animations 之前,确保两个子视图控制器都是容器内容的一部分,并且让当前的子视图控制器知道自己即将离开。在animations 期间,移动新的子视图控制器的视图到相应的位置同时移除先前的子视图控制器的视图。在animation 完成后,完成了子视图控制器的移除。
Listing5-3 展示了怎样使用transition animation 来交换两个子视图控制器。在这个例子中,新的视图控制器被animate 到被当前已存在的子视图控制器占据的矩形区域,该子视图控制器被移除屏幕外。在animations 完成之后,completion block 从容器中移除子视图控制器。在这个例子中,transitionFromViewController:toViewController:duration:options:animations:completion: 方法自动地更新容器的视图层次,所以不需要你自己来手动的添加和移除这些视图。
Listing5-3 在两个子视图控制器之间切换
为Children 管理appearance 更新
在添加一个子视图控制器到容器后,容器会自动发送appearance-related 信息给该子视图控制器。这是你想要的正常行为,因为它可以确保所有的事件都被正常的发送。然而,有时候默认的行为可能以一种对容器来说不合理的顺序来发送这些事件。例如,如果多个children 同时改变他们的视图状态,你可能想要合并这些改变,让appearance callbacks 以一种更合理的顺序同时发生。
为了接管appearance callbacks 的责任,在你的容器视图控制器中重载 shouldAutomaticallyForwardAppearanceMethods
方法,并且返回NO,如Listing5-4那样。返回NO,让UIKit知道你的容器视图控制器用自己的appearance 来通知它的children这种改变。
Listing5-4 Disabling automatic appearance forwarding
- (BOOL) shouldAutomaticallyForwardAppearanceMethods {
return NO;
}
当一个appearance 切换发生时,视情况调用子视图控制器的 beginAppearanceTransition:animated:
方法或者endAppearanceTransition 方法。例如,如果你的容器中有一个单一的视图控制器引用,通过一个child属性。你的容器应该发送下面Listing5-5中的信息给子视图控制器。
Listing 5-5Forwarding appearance messages when the container appears or disappears
-(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
属性返回的视图。容易应该从不访问子视图控制器的其它视图。
- 子视图控制器应该最小限度的了解它们的容器。 子视图控制器应该聚焦在自己的内容中,如果容器允许它的行为可以受另一个子视图控制器的影响,那么它应该使用delegation 设计模式来管理这些交互。
- 首先使用regular 视图来设计你的容器。 使用regular 视图(而不是来自子视图控制器的视图)给你一个在简单的环境中测试布局约束和animated transitions 的机会。当regular 视图如期望中的那样工作时,在你的子视图控制器中寻找一个合适的视图来交换它。
子视图控制器的委托控制
一个容器视图控制器可以delegate 它自己的appearance 的一些方面给一个或者多个它的子视图控制器。你可以用下面这些方法delegate control:
- 让一个子视图控制器决定状态条的风格。 为了delegate 状态条appearance 给一个子视图控制器,可以在你的容器视图控制器中重载一个或者两个childViewControllerForStatusBarStyle
and childViewControllerForStatusBarHidden
方法。
- 让子视图控制器指定自己的preferred size。 一个有灵活布局的容器可以使用子视图控制器自己的preferredContentSize 属性来帮助决定子视图的size。