容器视图控制器可以将多个视图控制器的内容结合到一个用户界面。容器视图控制器通常用于促进导航和基于现有内容创建新的用户界面。UIKit中容器视图控制器的例子包括UINavigationController
, UITabBarController
和UISplitViewController
,所有这些视图方面在不同用户界面直接导航。
设计自定义容器控制器
容器视图控制器如同其他任何内容视图控制器那样管理一个根视图和一些内容。不同的是容器视图控制器的部分内容来自其他视图控制器。内容仅限于其他视图控制器的视图,它嵌入到自己的视图层级。容器视图控制器设置任何嵌入视图的大小和位置,但原始视图控制器仍然管理这些视图中的内容。
当设计自己的容器视图控制器时,你需要理解容器和包含视图控制器。视图控制器间的关系可以帮助了解它们的内容appearance在屏幕上,容器在内部如何管理它们。在设计过程中,自问以下问题:
· 容器的作用及容器的子类的作用?
· 能同时显示多少个子视图?
· 同级视图控制器之间的关系(如果有)?
· 子视图控制器是如何添加到容器中,或者从容器中删除?
· 可以修改子视图的大小和位置?在什么情况下会发生改变?
· 容器提供任何装饰或导航视图?
· 在容器和子视图间需要什么样的通信方式?容器需要报告特定事件到子视图或者UIViewController类定义的标准视图?
· 容器的appearance是否可以以不同方式配置?如果可以,怎样配置?
在定义了各种对象的作用后,容器视图控制器的实现相对简单。UIKit唯一的要求就是在容器视图控制器和其他子类视图控制器之间构建一个正式的父子关系。父子关系确保子视图接收到任何相关的系统消息。除此之外,大部分的实际工作发生在布局和管理包含的视图过程中,每个视图都不一样。你可以在容器中的任何区域放置视图,设置视图的大小。也可以添加自定义视图到视图层级结构用于装饰或者帮助导航。
例子:导航控制器
UINavigationController 对象通过层次结构数据集支持导航。一个导航界面一次present一个子视图控制器。界面顶部的导航栏显示数据层次结构的当前位置并显示一个后退按钮返回到上一级别。
视图控制器之间的导航由导航控制器和其子类管理。当用户与按钮或子视图控制器表中某行交互时,子类访问导航控制器push一个新视图控制器。子类处理新视图控制器内容,但导航控制器管理过度动画。导航控制器也管理导航栏,显示一个返回按钮,用于dismiss顶层视图控制器。
图5-1 展示了导航控制器和其视图的结构。大部分内容区域是由最顶层子视图控制器和小部分导航栏组成。
在紧凑和常规环境中,导航控制器一次只显示一个子视图控制器。导航控制器调整其子视图适应可用空间。
例子:分屏视图控制器
UISplitViewController对象以主从结构显示两个视图控制器的内容。在这种布局中,(主)视图控制器决定其他视图控制器显示的细节。两个视图控制器是否可见可以由当前环境配置。在常规水平环境中,分屏视图控制器可以并排显示两个子视图控制器,或者隐藏主视图并在需要时显示。在紧凑环境中,分屏视图控制器一次只显示一个视图控制器。
图5-2 显示了分屏视图界面结构和在常规水平环境中的视图。分屏视图控制器默认情况下只有容器视图。在这个例子中,两个子视图并排显示。子视图的大小可配置,主视图是否可见也可以配置。
在界面构建器中配置一个容器
在设计时,要创建一个父子容器关系,将容器视图对象添加到你的故事版中,如图5-3。容器视图对象是一个占位符对象,用来代表子视图控制器的内容。根据容器中的其他视图,用该视图来控制子视图控制器的大小和位置。
当你加载一个有一个或多个容器视图的视图控制器时,界面构建器也加载这些视图的相关子视图控制器。这些子视图控制器必须在父视图实例化的时候实例化,这样才能创建父子关系。
如果你不适用界面构建器设置父子容器关系,你必须以编程的方式创建这些关系,将每个子视图添加到容器视图控制器上,详情见添加子视图控制器到内容( Adding a Child View Controller to Your Content)。
实现一个自定义容器视图控制器
实现一个容器视图控制器,你必须建立视图控制器和其子视图控制器之间的关系。在你视图管理任何子视图控制器的视图前必须建立这些父子关系。这样做让UIKit知道视图控制器在管理子视图的大小和位置。可以在界面构建器中创建或者以编程的方式创建。当以编程方式创建时,必须显式的添加和删除子视图,作为视图控制器设置的一部分。
添加子视图控制器到内容
以编程的方式将子视图控制器合并到内容,通过以下几点创建相关视图控制器的父子关系:
调用容器视图控制器的 [addChildViewController:](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIViewController_Class/index.html#//apple_ref/occ/instm/UIViewController/addChildViewController:)方法
该方法告诉UIKit,容器视图控制器管理子视图控制器的视图。
2.添加子视图控制器的根视图到容器视图控制器的视图层次结构。
记得设置子视图的大小和位置。
3.添加约束管理子视图的根视图的大小和位置。
4.调用子视图控制器的didMoveToParentViewController:方法。
列表5-1展示了嵌入子视图控制器的容器。建立父子关系后,容器设置子视图控制器的frame,并将子视图添加到自己的视图层级中。设置子视图的frame很重要,确保视图在容器中正确显示。添加视图后,容器调用子视图的didMoveToParentViewController:
方法使子视图控制器响应视图的变更。
列表5-1 添加子视图控制器到容器
<pre><code>
-(void)displayContentController:(UIViewController*) content{
[self addChildViewController:content];
content.view.frame = [self frameForContentController];
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self];
}
</pre></code>
在前面的例子中,注意你只调用子视图的didMoveToParentViewController:
方法。这时因为addChildViewController:
方法会调用 willMoveToParentViewController:
方法。必须调用是因为didMoveToParentViewController:
方法不会自己调用直到将子视图嵌入到容器视图层级中。
当使用自动布局时,在添加子视图到容器视图层级结构后,在容器和孩子之间建立约束。约束会影响子视图控制器根视图的大小和位置。不改变根视图的内容或子视图层级结构中的其他视图。
删除子视图控制器
从内容中删除子视图控制器,通过以下几点删除视图控制器之间的父子关系:
调用子视图控制器的 [willMoveToParentViewController:](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIViewController_Class/index.html#//apple_ref/occ/instm/UIViewController/willMoveToParentViewController:)方法并设置值为nil。
删除子视图控制器根视图配置的任何约束。
从容器视图层级中删除子视图控制器根视图。
调用子视图的 [removeFromParentViewController](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIViewController_Class/index.html#//apple_ref/occ/instm/UIViewController/removeFromParentViewController)方法删除父子关系。
删除子视图控制器会永久删除父视图和子视图之间的关系。当你不再需要它时,删除子视图控制器。例如,当新视图控制器push到导航堆栈时,导航控制器不移除当前子视图控制器。只有当他们被pop出栈时才会被删除。
列表5-2 展示了如何从容器中删除子视图控制器。调用willMoveToParentViewController:
方法,使子视图控制器有改变的机会。removeFromParentViewController
方法同样会调用子视图的didMoveToParentViewController: 方法,传递该方法值为nil。设置父视图控制器为nil,从容器中删除子视图。
列表5-2 从容器中删除子视图控制器
<pre><code>
-(void)hideContentController: (UIViewController*) content {
[content willMoveToParentViewController:nil];
[content.view removeFromSuperview];
[content removeFromParentViewController];
}
</pre></code>
子视图控制器间的过渡
当需要子视图控制器过渡到另一个视图控制器的动画,结合子视图控制器的添加和删除到过渡动画过程。在动画之前,确保两个子视图控制器是内容的一部分,让当前的子视图消失。在动画过程中,移动新子视图控制器到相应的位置并删除旧的子视图控制器。在动画完成之后,删除子视图控制器。
利用过渡动画,列表5-3 展示了如何子视图控制器的切换。在这个例子中,新视图控制器进入到当前子视图控制器占据的矩形中,该子视图移动到屏幕之外。在动画完成后,完成block从容器中删除子视图控制器。在这个例子中,transitionFromViewController:toViewController:duration:options:animations:completion: 方法自动更新容器视图层级结构,所以不需要自己添加和删除视图。
列表5-3 两个子视图控制器间的过渡
<pre><code>
- (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];
}];
}
</pre></code>
管理子视图控制器appearance更新
添加子视图控制器到容器后,容器自动将相关appearance消息传递给子视图。这通常是你希望的行为,因为它确保所有事件正确发送。然而,有时候默认行为可能按顺序发送一些不影响容器的事件。例如,如果多个孩子同时改变他们的视图状态,希望巩固这些变化,这样可以同时按照逻辑顺序执行appearance回调。
为了完成appearance回调,覆盖容器视图控制器的shouldAutomaticallyForwardAppearanceMethods 方法并返回NO,如列表5-4所示。返回NO使UIKit知道容器视图控制器通知其子视图控制器appearance变化。
列表5-4 禁用自动转发
<pre><code>
-(BOOL) shouldAutomaticallyForwardAppearanceMethods {
return NO;
}
</pre></code>
当appearance发生变化时,调用子视图控制器的 beginAppearanceTransition:animated:或endAppearanceTransition方法。例如,如果容器有子视图引用子视图属性,容器将这些消息转发给子视图,如列表5-5中所示。
列表5-5 当容器出现或消失转发appearance消息
<pre><code>
- -(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];
- }
</pre></code>
构建容器视图控制器的建议
设计、开发和测试新容器视图控制器需要时间。尽管个体行为简单,控制器作为一个整体可能相当复杂,实现自己的容器类时考虑以下建议:
· 只访问子视图控制器的根视图。容器应该只访问每个子视图控制器的根视图,即返回子视图的视图属性。它不应该访问子视图控制器的其他视图。
· 子视图控制器不用了解容器。子视图控制器应该关注自己的内容。如果容器允许其行为被子视图控制器影响,应该使用代理模式来管理这些交互。
· 优先使用普通视图设计你的容器。使用普通视图(而不是子视图控制器的视图)让你可以测试布局约束和简单环境过渡动画。当普通视图如预期那样工作,移除子视图控制器的视图。
委托控制子视图控制器
容器视图控制器可以委托appearance到一个或多个子视图控制器中。可以通过以下几点委托控制。
· 让子视图控制器决定状态栏风格。委托状态栏appearance到子视图,覆盖容器视图控制器的childViewControllerForStatusBarStyle
和childViewControllerForStatusBarHidden
方法。
· 让子视图控制器指定自己的大小。灵活布局的容器可以使用子视图自己的preferredContentSize属性,以帮助决定子视图的大小。