前言
概述
容器类ViewController可以将多个ViewController的内容结合在一起展示在一个用户界面上。容器类ViewController经常被用来促进导航和根据现有内容创建新的用户交互界面类型。UIKIt框架里包含的容器类ViewController有:UINavigationController, UITabBarController, 和 UISplitViewController,他们负责着你的用户界面中不同的部分间的导航。
设计一个自定义的容器类ViewController
在大多数情况下,容器类ViewController和其他内容型ViewController一样,它管理着一个根视图和其他内容。不同的是,容器类ViewController从其他ViewController中获取内容。它所获取的内容依赖于其他ViewController的视图,它嵌入在自己的视图层级结构中。容器类型ViewController设置被嵌入视图的尺寸和位置,但是原先的ViewController仍然管理着这些视图的内容。
当设计自己的容器类ViewController的时候,一定要理解容器ViewController和被容器包含的ViewController之间的关系。这个关系可以帮你确定他们的内容是应当在屏幕上展示成什么样和你怎样在容器内部管理他们。在设计的过程中,带着如下几个疑问:
- 容器的角色是什么?它的子ViewController扮演者什么样的角色。
- 多少个子ViewController同时展示。
- 兄弟ViewController之间的关系。
- 如何在容器中添加和移除子ViewController。
- 子ViewController的尺寸和位置是否可以改变?在什么样的条件下这些改变会发生。
- 容器是否提供任何装饰或者导航相关的视图。
- 容器和它的子ViewController应该以何种方式沟通,除了UIViewController中定义的表中事件外,容器是否需要向它的子ViewController报告事件。
- 容器类型的外观能否被定义成不同的方式,如果可以,该怎么做。
在你定义了各个对象的角色之后,容器类ViewController的实现非常简单。UIKit唯一要求你做的就是在容器ViewController和子ViewController之间建立父子关系。这个父子关系确保子ViewController接收相关的系统信息。除此之外,大多数工作花费在在布局和管理被包含的视图,这依据不同的容器而定。你可以将视图放置在容器中的任何位置,定义你想要的任何尺寸。你也可以添加一个自定义的视图到视图的层级结构上来提供装饰或者导航的作用。
举例:Navigation Controller
UINavgationController
对象支持通过分层数据集方式进行导航。导航界面一次显示一个子视图控制器界面。界面顶部的导航栏显示数据层次结构中的当前位置,并显示返回按钮以返回上一个层级。如果想要到下一层下一个数据层级可能需要涉及到使用tableview或者button。
视图控制器之间的导航是由导航控制器及其子女共同管理的。当用户与子视图控制器的按钮或者表格进行交互时,子视图会要求导航控制器将新的视图控制器推入界面。当前子视图处理新视图控制器内容配置,但导航控制器管理过渡动画。导航控制器还管理导航栏,该导航栏显示用于解除最顶层视图控制器的后退按钮。
下图显示了导航控制器及其视图结构。大多数内容区域由最顶层的子视图控制器填充,导航栏只占用了一小部分。
无论在compact还是regular环境下,导航控制器一次都只显示一个子视图。导航控制器调整子视图以适应可用的空间。
举例:Split View Controller
UISplitViewController
对象以主要-细节布置两个视图控制器的内容。在这种安排下,一个视图控制器(主视图控制器)的内容决定了其他视图控制器显示的细节。两个视图控制器的可见性是可以配置的,但也受到当前环境的支配。在规则的水平环境下,UISplitViewController
可以同时显示两个子视图控制器,也可以根据需要隐藏主视图。在compact
环境下,UISplitViewController
一次只能显示一个视图控制器。
下图展示了Split View在水平环境下的视图结构。UISplitViewController
默认只包含它所容纳的视图。在这个例子中,两个子视图并排显示。子视图的显示区域是可配置的,主视图的可见性也是可配置的。
在Interface Builder中构建容器视图控制器
要在设计时创建父子容器关系,请将容器视图对象添加到storyboard
中,如下图所示。容器视图对象是一个代表子视图控制器内容的占位符对象。使用该视图根据容器中的其他视图定位其在子根视图中的位置。
当你使用容器视图加载一个或者多个视图控制器时,Interface Builder
同时加载与这些视图相关的试图控制器。子视图控制器必须与父视图控制器同时初始化以便于创建合适的父子关系。
如果你不使用Interface Builder
构建父子容器关系,你需要通过编写代码添加每个子视图控制器到容器试图控制中,如Adding a Child View Controller to Your Content文档中所表述的一样。
实现一个自定义的容器视图控制器
为了实现容器类视图控制器,你需要建议你的容器视图控制器与其子视图控制器之间的父子关系。在你管理子视图控制器的视图之前,建立这种父子关系是必要的。这些操作让UIKit知道你的视图控制器管理着子视图控制器的位置和大小。你可以通过xib或者手写代码方法建立这种父子关系。当你使用编码的方式建立这种关系时,你明确的将添加和移除子视图控制器作为你的视图控制器的一部分设置。
向你的容器中添加一个子视图控制器
要通过编码的方式将子视图控制器添加你的内容中,可以通过以下几个步骤来完成:
-
通过你的容器视图控制器调用
addChildViewController:
方法该方法告知UIKit你的容器视图控制器管理子视图控制器的视图。
-
将子视图控制器的根视图添加到视图层级。
同时需要设定子视图控制器的根视图在容器中位置和大小。
添加一些约束来管理子试图控制器根视图的大小和位置。
调用子视图控制的
didMoveToParentViewController
方法。
下面的代码展示了如何将子视图控制器嵌入到容器中。在建立父子关系之后,容器试图控制器设置子视图控制器的frame,并将子视图控制的根视图添加到自己的层级结构中。设置子视图的frame非常重要,并确保视图在容器中正确显示。在添加视图之后,容器调用子视图控制器的didMoveToParentViewController:
方法,以使得子视图控制器有机会响应视图所有权的更改。
- (void)displayContentController:(UIViewController *)content {
[self addChildViewController:content];
content.view.frame = [self frameForContentController];
[self.view addSubView:self.currentClientView];
[content didMoveToParentViewController:self];
}
在前面的例子中,请注意你只调用了子视图控制器的didMoveToParentViewController
方法。这是因为addChildViewController:
方法会调用子视图控制的willMoveToParentViewController:
方法。需要你主动调用didMoveToParentViewController:
的原因是:该方法只能在你将子视图嵌入到容器的层级结构之后才能调用。
当你使用自动布局的时候,在你将子视图添加到容器的层级结构之后设置容器与子视图的约束。该约束只能影响子视图控制器的根视图的大小和位置。不要更改子视图层次结构中根视图或者契合其他视图的内容。
移除子视图控制器
要从内容中删除子视图控制器,请通过执行以下操作来删除视图控制器之间的父子关系:
- 调用子视图控制的
willMoveToParentViewController:
方法,参数为nil。 - 删除子视图控制器的根视图的所有约束。
- 将子视图控制器的根视图从容器的层级结构中移除。
- 调用子视图控制器的
removeFromParentViewController
方法来结束父子关系。
删除子视图控制器将永久切断父子关系。仅当您不再需要引用子视图控制器时,才可以删除它。例如,当新导航控制器被推入导航堆栈时,导航控制器不会移除其当前的子视图控制器。只有当它们从堆栈中弹出时才会删除它们。
下面代码展示了如何从容器中移除子视图控制器。调用willMoveToParentViewController
方法并传入参数nil使得子视图控制器有机会准备改变。removeFromParentViewController
方法会调用子视图控制器的didMoveToParentViewController:
方法,并传递nil参数。设置父视图控制器为nil最终将子视图控制器从容器中移除。
- (void)hideContentController: (UIViewController *)content {
[content willMoveToParentViewController:nil];
[content.view removeFromSuperView];
[content removeFromParentViewController];
}
子视图之间的转场
当你想使用一个动画来替换子视图被另一个子视图替换的操作时,将子视图控制器的添加和移除操作合并在一个动画过程中。在动画执行之前,确保所有的子视图控制器是你的内容的一部分,并且让当前子视图知道它将如何离场。在动画执行期间,将新视图移动到指定的位置并将旧视图移除,在动画执行完成之后,完全删除子视图控制器。
下面代码展示了如何使用动画将子视图控制器切换到另一个子视图控制器。在这个例子中,新的视图控制器移动到当前视图控制器显示的矩形区域中,旧的视图控制器移动到屏幕外。在动画执行结束之后,完成的Block中将子视图控制器从容器中完全移除。在这个例子中,transitionFromViewController:toViewController:duration:options:animations:completion:
方法自动更新容器的视图层级结构,所以你不需要自己添加和移除视图。
- (void)cycleFromViewController:(UIViewController *) oldVc
toViewController:(UIViewController *)newVc {
[oldVc willMoveToParentViewController:nil];
[self addChildViewController:newVc];
newVc.view.frame = [self newViewStartFrame];
CGRect endFrame = [self oldViewEndFrame];
[self transitionFromViewController:oldVc toViewController: newVc duration: 0.25
options:0 animations:^{
newVc.view.frame = oldVc.view.frame;
oldVc.view.frame = endFrame;
} completion:^(BOOL finished){
[oldVc removeFromeParentViewController];
[newVc didMoveToParentViewController];
}];
}
管理子视图的显示更新
在容器添加了子视图控制器之后,容器自动将显示相关的消息转发给子视图控制器。这正是你所想要的,因为它确保了所有的事件正确的响应。然而,有时默认行为可能会对您的容器无意义的顺序发送这些事件。例如:如果多个子视图控制器同时修了他们的状态,你可能需要合并这些更改,以便外观回调都以更合理的顺序同时发送。
为了接管外观回调的责任,在容器视图控制器中重写shouldAutomaticallyForwardAppearanceMethods
方法并返回NO,如下面代码所示。返回NO使得UIKIt知道你的容器视图控制器通知它的子视图控制关于外观的改变。
-(BOOL)shouldAutomaticallyForwardAppearanceMethods {
return NO;
}
当一个外观的约束发送,在适当的时候调用子视图控制器的beginAppearanceTransition(_:animated:)
和endAppearanceTransition()
方法。举例:如果你的容器只有一个子视图控制器,并被child所引用,你的容器需要转发这些消息给子视图控制器,如下所示:
- (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];
}
构建容器类型的ViewController的一些建议
设计、开发、测试一个新的容器类ViewController是需要花费一些时间的,虽然单个的功能实现起来非常简单,但Controller作为一个整体就十分复杂了,当你实现自己的容器类的时候,考虑下面几点提示:
- 仅访问子ViewController的根视图。容器应该只访问每个子ViewController的根视图,也就是子ViewController的view属性。容器不应该访问子ViewController的其他视图。
- 子ViewController应该知道容器类的最少信息。子ViewController应该关注自己的内容。如果容器允许它自己的行为响应子ViewController,应该使用代理模式来管理这些交互。
- 首先使用常规的视图设计你的容器。使用常规的视图(而不是子ViewController里的View)让你在一个简单的环境中测试约束布局和动画转化。当常规视图符合期望的时候,再将它切换到子ViewController中。
将控制委派给子ViewController
容器类ViewController可以将一些影响它自身显示效果的操作代理给一个或者多个子ViewController。可以使用下面方式下发控制权:
- 让子ViewController决定status bar 的类型。将status bar的显示委派给子ViewController,可以在子ViewController覆写一个或者所有childViewControllerForStatusBarStyle 和 childViewControllerForStatusBarHidden 方法。
- 让子ViewController指定它自己的适合的尺寸。容器可以使用子perferredContentSize属性帮助您决定子ViewController的尺寸。