容器视图控制器是将来自多个视图控制器的内容组合到单个用户界面中的一种方式。 容器视图控制器最常用于导航并基于现有内容创建新的用户界面类型。 UIKit中的容器视图控制器的示例包括UINavigationController,UITabBarController和UISplitViewController,所有这些都有助于在用户界面的不同部分之间进行导航。
设计自定义容器视图控制器
在几乎每种方式中,容器视图控制器像任何其他内容视图控制器一样,它管理根视图和一些内容。 区别是容器视图控制器从其他视图控制器获取其内容的一部分。 它获得的内容仅限于其他视图控制器的视图,它嵌入在自己的视图层次结构中。 容器视图控制器设置任何嵌入视图的大小和位置,但原始视图控制器仍然管理这些视图中的内容。
当设计自己的容器视图控制器时,总是理解容器和包含的视图控制器之间的关系。 视图控制器的关系可以帮助了解他们的内容应该如何在屏幕上显示,以及你的容器在内部如何管理它们。 在设计过程中,请问自己以下问题:
- 容器的作用是什么?它的子视图控制器们扮演什么角色?
- 同时显示多少个子视图控制器?
- 兄弟视图控制器之间的关系(如果有)是什么?
- 如何向容器添加或从容器中删除子视图控制器?
- 子视图控制器的大小或位置可以改变吗? 在什么条件下这些变化发生?
- 容器是否提供了自己的装饰或导航相关视图?
- 容器与其子视图控制器之间需要什么样的沟通? 容器是否需要向它的孩子报告特定的事件,而不是由UIViewController类定义的标准事件?
- 容器的外观可以用不同的方式配置吗? 如果是,如何?
在定义了各种对象的角色后,容器视图控制器的实现相对简单。 UIKit的唯一要求是在容器视图控制器和任何子视图控制器之间建立正式的父子关系。 父子关系确保子代接收到任何相关的系统消息。 除此之外,大多数实际工作发生在包含视图的布局和管理期间,这对于每个容器是不同的。 您可以将视图放置在容器的内容区域的任何位置,并根据需要调整这些视图的大小。 您还可以向视图层次结构添加自定义视图,以提供装饰或辅助导航。
示例:导航控制器
UINavigationController对象支持通过分层数据集的导航。 导航界面一次呈现一个子视图控制器。 界面顶部的导航栏显示数据层次结构中的当前位置,并显示后退按钮以向后移动一级。 向下浏览到数据层次结构留给子视图控制器,并且可以涉及使用表或按钮。
视图控制器之间的导航由导航控制器及其孩子共同管理。 当用户与子视图控制器的按钮或表行交互时,孩子要求导航控制器将新的视图控制器推入视图。 子处理新的视图控制器的内容的配置,但是导航控制器管理过渡动画。 导航控制器还管理导航栏,其显示用于解除最顶层视图控制器的后退按钮。
图5-1显示了导航控制器的结构及其视图。 大多数内容区域由最上面的子视图控制器填充,并且导航栏仅占据一小部分。
图5-1导航界面的结构
在紧凑和常规环境中,导航控制器一次只显示一个子视图控制器。 导航控制器调整其子级以适合可用空间。
示例:分割视图控制器
UISplitViewController对象以主 - 细部安排显示两个视图控制器的内容。 在这种布置中,一个视图控制器(主控)的内容确定由另一个视图控制器显示什么细节。 两个视图控制器的可见性是可配置的,但也受当前环境的控制。 在定期水平环境中,分割视图控制器可以并排显示两个子视图控制器,或者它可以隐藏主控器并根据需要显示它。 在紧凑环境中,拆分视图控制器一次只显示一个视图控制器。
图5-2显示了在定期水平环境中的拆分视图接口及其视图的结构。 默认情况下,拆分视图控制器本身只有其容器视图。 在本示例中,两个子视图并排显示。 子视图的大小是可配置的,主视图的可见性也是可配置的。
图5-2分割视图界面
在Interface Builder中配置容器
要在设计时创建父子容器关系,请将容器视图对象添加到故事板场景中,如图5-3所示。 容器视图对象是表示子视图控制器的内容的占位符对象。 使用该视图来确定子视图相对于容器中其他视图的大小和位置。
图5-3在Interface Builder中添加容器视图
当您加载具有一个或多个容器视图的视图控制器时,Interface Builder也会加载与这些视图相关联的子视图控制器。 子代必须与父代同时实例化,以便可以创建适当的父子关系。
如果不使用Interface Builder设置父子容器关系,则必须通过将每个子代添加到容器视图控制器来编程创建这些关系,如将子视图控制器添加到内容中所述。
实现自定义容器视图控制器
要实现容器视图控制器,必须在视图控制器与其子视图控制器之间建立关系。 在尝试管理任何子视图控制器的视图之前,需要建立这些父子关系。 这样做让UIKit知道你的视图控制器正在管理孩子的大小和位置。 您可以在Interface Builder中创建这些关系或以编程方式创建它们。 在以编程方式创建父子关系时,您可以在视图控制器设置中显式添加和删除子视图控制器。
向你的内容添加子视图控制器
要以编程方式将子视图控制器合并到您的内容中,请通过执行以下操作在相关视图控制器之间创建父子关系:
- 调用容器视图控制器的addChildViewController:方法。
这个方法告诉UIKit你的容器视图控制器现在正在管理子视图控制器的视图。
- 将子级的根视图添加到容器的视图层次结构中。始终记住设置孩子的大小和位置作为此过程的一部分。
- 添加任何约束以管理子级的根视图的大小和位置。
- 调用子视图控制器的didMoveToParentViewController:方法。
清单5-1显示了容器如何在其容器中嵌入子视图控制器。 在建立父子关系之后,容器设置其子代的帧,并将子代的视图添加到其自己的视图层级中。 设置子视图的帧大小很重要,并确保视图在容器中正确显示。 在添加视图之后,容器调用子节点的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:方法的原因是,在将子视图嵌入到容器的视图层次结构中之后才能调用该方法。
使用自动布局时,在将子容器添加到容器的视图层次结构后,在容器和子项之间设置约束。 您的约束应该影响只有子控制器的根视图的大小和位置。 不要更改根视图或子视图层次结构中任何其他视图的内容。
删除子视图控制器
要从内容中删除子视图控制器,请通过执行以下操作删除视图控制器之间的父子关系:
- 调用子控制器的willMoveToParentViewController:方法的值为nil。
- 删除你使用子项的根视图配置的所有约束。
- 从容器的视图层次结构中删除子项的根视图。
- 调用子控制器的removeFromParentViewController方法来完成父子关系的结束。
删除子视图控制器会永久切断父级和子级之间的关系。 仅当不再需要引用子视图控制器时,才删除子视图控制器。 例如,当新的子视图控制器被推入导航栈时,导航控制器不会删除其当前的子视图控制器。 它只有当它们从堆栈弹出时才会删除它们。
清单5-2显示了如何从其容器中删除子视图控制器。 调用值为nil的willMoveToParentViewController:方法为子视图控制器提供了准备更改的机会。 removeFromParentViewController方法还调用子进程的didMoveToParentViewController:方法,传递该方法的值为nil。 将父视图控制器设置为nil将完成从容器中移除子视图。
- (void) hideContentController: (UIViewController*) content {
[content willMoveToParentViewController:nil];
[content.view removeFromSuperview];
[content removeFromParentViewController];
}
子视图控制器之间的转换
当你想要用一个子视图控制器替换另一个子视图控制器时,将子视图控制器添加和删除到过渡动画过程中。 在动画之前,确保两个子视图控制器是您的内容的一部分,但让当前的孩子知道它即将离开。 在你的动画期间,将新孩子的视图移动到位,并删除旧孩子的视图。 在动画完成后,完成子视图控制器的删除。
代码清单5-3显示了如何使用过渡动画来交换一个子视图控制器的示例。 在该示例中,新视图控制器被动画化到现有子视图控制器当前占用的矩形,其被移离屏幕。 动画完成后,完成块从容器中删除子视图控制器。 在此示例中,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方法。 例如,如果您的容器有一个由子属性引用的单个子项,则您的容器会将这些消息转发给子项,如代码清单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];
}
构建容器视图控制器的建议
设计,开发和测试新的容器视图控制器需要时间。 虽然各个行为是直接的,但控制器作为一个整体可能相当复杂。 在实现自己的容器类时,请考虑以下提示:
- 仅访问子视图控制器的根视图。 容器应该只访问每个子项的根视图,即由子视图属性返回的视图。 它不应该访问任何孩子的其他视图。
- 子视图控制器应该具有最小的容器知识。 子视图控制器应该专注于自己的内容。 如果容器允许它的行为受到孩子的影响,它应该使用委托设计模式来管理这些交互。
- 首先使用常规视图设计容器。 使用常规视图(而不是子视图控制器的视图)可以在简化的环境中测试布局约束和动画过渡。 当常规视图按预期工作时,将其交换为您的子视图控制器的视图。
将控制委派给子视图控制器
容器视图控制器可以将其自身外观的一些方面委托给其一个或多个子代。 您可以通过以下方式委派控制:
- 让子视图控制器确定状态栏样式。 要将状态栏外观委派给子级,请覆盖容器视图控制器中的childViewControllerForStatusBarStyle和childViewControllerForStatusBarHidden方法中的一个或两个。
- 让子视图控制器指定自己的首选大小。 具有灵活布局的容器可以使用子代自己的preferredContentSize属性来帮助确定子代的大小。