Custom Container View Controller

什么是Container View Controller?苹果文档是这么描述的:

 A container view controller contains content owned by other view controllers.

也就是说一个View Controller显示的某部分内容属于另一个View Controller,那么这个View Controller就是一个Container,比如UIKit中的UINavigationController,UITabBarController。

在iOS 5之前苹果是不允许出现自定义的Container的 ,也就是说你创建的一个View Controller的view不能包含另一个View Controller的view,这对于逻辑复杂的界面来说,不易于功能拆分。也许曾经你为了某个公用的显示逻辑,直接将某个View Controller的view添加到另一个View Controller的view上,然后发现可以正常显示和使用,但实际上这种行为是非常危险的。

iOS 5.0 开始支持Custom Container View Controller,开放了用于构建自定义Container的接口。如果你想创建一个自己的Container,那么有一些概念还得弄清楚。Container的主要职责就是管理一个或多个Child View Controller的展示的生命周期,需要传递显示以及旋转相关的回调。

其实显示或者旋转的回调的触发的源头来自于window,一个app首先有一个主window,初始化的时候需要给这个主window指定一个rootViewController,window会将显示相关的回调(viewWillAppear:, viewWillDisappear:, viewDidAppear:, or viewDidDisappear: )以及旋转相关的回调(willRotateToInterfaceOrientation:duration: ,willAnimateRotationToInterfaceOrientation:duration:, didRotateFromInterfaceOrientation:)传递给rootViewController。rootViewController需要再将这些callbacks的调用传递给它的Child View Controllers。

一. 父子关系范式

实现一个Custom Container View Controller并不是一个简单的事情,主要分为两个阶段:父子关系的建立以及父子关系的解除。如果pVC将cVC的view添加为自己的subview,那么cVC必须为pVC的Child View Controller,而反过来则不一定成立,比如UINavigationController,一个View Controller被push进来后便和navigationController建立父子关系了,但是只有最上面的View Controller 是显示着的,底下的View Controller的view则被移出了容器的view的显示层级,当一个View Controller被pop之后,便和navigationController解除了父子关系了。

展示一个名为content的child view controller:

  1. [self addChildViewController:content];  //1
  2. content.view.frame = [self frameForContentController];
  3. [self.view addSubview:self.currentClientView]; //2
  4. [content didMoveToParentViewController:self]; //3

1.将content添加为child view controller,addChildViewController:接口建立了逻辑上的父子关系,子可以通过parentViewController,访问其父VC,addChildViewController:接口的逻辑中会自动调用 [content willMoveToParentViewController:self];

2.建立父子关系后,便是将content的view加入到父VC的view hierarchy上,同时要决定的是 content的view显示的区域范围。

3.调用child的 didMoveToParentViewController: ,以通知child,完成了父子关系的建立

移除一个child view controller:

  1. [content willMoveToParentViewController:nil]; //1
  2. [content.view removeFromSuperview]; //2
  3. [content removeFromParentViewController]; //3

1.通知child,即将解除父子关系,从语义上也可以看出 child的parent即将为nil

2.将child的view从父VC的view的hierarchy中移除

3.通过removeFromParentViewController的调用真正的解除关系,removeFromParentViewController会自动调用 [content didMoveToParentViewController:nil]

二. appearance callbacks的传递

上面的实现中有一个问题,就是没看到那些appearance callbacks是如何传递的,答案就是appearance callbacks默认情况下是自动调用的,苹果框架底层帮你实现好了,也就是在上面的addSubview的时候,在subview真正加到父view之前,child的viewWillAppear将被调用,真正被add到父view之后,viewDidAppear会被调用。移除的过程中viewWillDisappear,viewDidDisappear的调用过程也是类似的。

有时候自动的appearance callbacks的调用并不能满足需求,比如child view的展示有一个动画的过程,这个时候我们并不想viewDidAppear的调用在addSubview的时候进行,而是等展示动画结束后再调用viewDidAppear。

也许你可能会提到 transitionFromViewController:toViewController:duration:options:animations:completion: 这个方法,会帮你自动处理view的add和remove,以及支持animations block,也能够保证在动画开始前调用willAppear或者willDisappear,在调用结束的时候调用didAppear,didDisappear,但是此方式也存在局限性,必须是两个新老子VC的切换,都不能为空,因为要保证新老VC拥有同一个parentViewController,且参数中的viewController不能是系统中的container,比如不能是UINavigationController或者UITabbarController等。

所以如果你要自己写一个界面容器往往用不了appearence callbacks自动调用的特性,需要将此特性关闭,然后自己去精确控制appearance callbacks的调用时机。

那如何关闭appearance callbacks的自动传递的特性呢?

在iOS 5.x中你需要覆盖automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers,然后返回NO,iOS6+中你需要覆盖 shouldAutomaticallyForwardAppearanceMethods方法并返回NO。

手动传递的时候你并不能直接去调用child 的viewWillAppear或者viewDidAppear这些方法,而是需要使用 beginAppearanceTransition:animated:和endAppearanceTransition接口来间接触发那些appearance callbacks,且begin和end必须成对出现。

[content beginAppearanceTransition:YES animated:animated]触发content的viewWillAppear,[content beginAppearanceTransition:NO animated:animated]触发content的viewWillDisappear,和他们配套的[content endAppearanceTransition]分别触发viewDidAppear和viewDidDisappear。 (AppearanceTransition的这两个接口之前在苹果描述的文档中一开始还存在问题,因为文档中一开始说是iOS5不支持这两个接口,其实是支持的,后来苹果纠正了文档中的这个错误)。

三. rotation callbacks的传递

也许在iPhone上很少要关心的屏幕旋转问题的,但是大屏幕的iPad上就不同了,很多时候你需要关心横竖屏。rotation callbacks 一般情况下只需要关心三个方法 willRotateToInterfaceOrientation:duration:在旋转开始前,此方法会被调用;willAnimateRotationToInterfaceOrientation:duration: 此方法的调用在旋转动画block的内部,也就是说在此方法中的代码会作为旋转animation block的一部分;didRotateFromInterfaceOrientation:此方法会在旋转结束时被调用。而作为view controller container 就要肩负起旋转的决策以及旋转的callbacks的传递的责任。

当使用框架的自动传递的特性的时候,作为容器的view controller 会自动 将这些方法传递给所有的child viewcontrollers, 有时候你可能不需要传递给所有的child viewcontroller,而只需要传递给正在显示的child viewcontroller,那么你就需要禁掉旋转回调自动传递的特性,和禁掉appearance callbacks自动传递的方式类似,需要覆盖相关方法并返回NO,在iOS5.x中,appearance callbacks和rotation callbacks禁掉是公用一个方法的就是 automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers,在iOS6之后分成两个独立的方法,旋转的则是 shouldAutomaticallyForwardRotationMethods。

旋转相关的除了上面的几个rotation callbacks方法外,还有一个十分重要的概念,就是一个view controller可以决定自己是否支持当前取向的旋转,这个东西在iOS6前后的实现方式还不一样,iOS6之前使用的方法是 shouldAutorotateToInterfaceOrientation,就是一个view controller覆盖此方法,根据传入的即将旋转的取向的参数,来决定是否旋转。而iOS6.0之后的实现则拆分成两个方法 shouldAutorotate和supportedInterfaceOrientations,前者决定再旋转的时候是否去根据supportedInterfaceOrientations所支持的取向来决定是否旋转,也就是说如果shouldAutorotate返回YES的时候,才会去调用supportedInterfaceOrientations检查当前view controller支持的取向,如果当前取向在支持的范围中,则进行旋转,如果不在则不旋转;而当shouldAutorotate返回NO的时候,则根本不会去管supportedInterfaceOrientations这个方法,反正是不会跟着设备旋转就是了。

而作为界面容器你要注意的就是你需要去检查你的child view controller,检查他们对横竖屏的支持情况,以便容器自己决策在横竖屏旋转时候是否支持当前的取向,和上面的callbacks传递的方向相比,这其实是一个反向的传递。

四. 创建自己的容器基类

当你需要构建自己的Container View Controller的时候,每一个Container都会有一些相同的逻辑,如果你每一个都写一遍会存在很多重复代码,所以最好你创建一个Container基类,去实现容器都需要的逻辑。那到底有哪些逻辑是每一个Container都需要做的呢?关闭Appearance和Rotation相关方法的自动传递;当Container的Appearance和Rotation相关方法被调用时,需要将方法传递给相关的Child View Controller;以及当前Container是否支持旋转的决策逻辑等。下面为一个容器基类的示范:

  1. #import "ContainerBaseController.h"
  2. @implementation ContainerBaseController
  3. #pragma mark -
  4. #pragma mark Overrides
  5. //NS_DEPRECATED_IOS(5_0,6_0)
  6. - (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers{
  7. return NO;
  8. }
  9. //NS_AVAILABLE_IOS(6_0)
  10. - (BOOL)shouldAutomaticallyForwardAppearanceMethods{
  11. return NO;
  12. }
  13. //NS_AVAILABLE_IOS(6_0)
  14. - (BOOL)shouldAutomaticallyForwardRotationMethods{
  15. return NO;
  16. }
  17. - (void)viewWillAppear:(BOOL)animated{
  18. [super viewWillAppear:animated];
  19. NSArray *viewControllers = [self childViewControllersWithAppearanceCallbackAutoForward];
  20. for (UIViewController *viewController in viewControllers) {
  21. [viewController beginAppearanceTransition:YES animated:animated];
  22. }
  23. }
  24. - (void)viewDidAppear:(BOOL)animated{
  25. [super viewDidAppear:animated];
  26. NSArray *viewControllers = [self childViewControllersWithAppearanceCallbackAutoForward];
  27. for (UIViewController *viewController in viewControllers) {
  28. [viewController endAppearanceTransition];
  29. }
  30. }
  31. - (void)viewWillDisappear:(BOOL)animated{
  32. [super viewWillDisappear:animated];
  33. NSArray *viewControllers = [self childViewControllersWithAppearanceCallbackAutoForward];
  34. for (UIViewController *viewController in viewControllers) {
  35. [viewController beginAppearanceTransition:NO animated:animated];
  36. }
  37. }
  38. - (void)viewDidDisappear:(BOOL)animated{
  39. [super viewDidDisappear:animated];
  40. NSArray *viewControllers = [self childViewControllersWithAppearanceCallbackAutoForward];
  41. for (UIViewController *viewController in viewControllers) {
  42. [viewController endAppearanceTransition];
  43. }
  44. }
  45. - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{
  46. [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
  47. NSArray *viewControllers = [self childViewControllersWithRotationCallbackAutoForward];
  48. for (UIViewController *viewController in viewControllers) {
  49. [viewController willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
  50. }
  51. }
  52. - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{
  53. [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
  54. NSArray *viewControllers = [self childViewControllersWithRotationCallbackAutoForward];
  55. for (UIViewController *viewController in viewControllers) {
  56. [viewController willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
  57. }
  58. }
  59. - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
  60. [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
  61. NSArray *viewControllers = [self childViewControllersWithRotationCallbackAutoForward];
  62. for (UIViewController *viewController in viewControllers) {
  63. [viewController didRotateFromInterfaceOrientation:fromInterfaceOrientation];
  64. }
  65. }
  66. /*
  67. NS_AVAILABLE_IOS(6_0)
  68. 向下查看和旋转相关的ChildViewController的shouldAutorotate的值
  69. 只有所有相关的子VC都支持Autorotate,才返回YES
  70. */
  71. - (BOOL)shouldAutorotate{
  72. NSArray *viewControllers = [self childViewControllersWithRotationCallbackAutoForward];
  73. BOOL shouldAutorotate = YES;
  74. for (UIViewController *viewController in viewControllers) {
  75. shouldAutorotate = shouldAutorotate &&  [viewController shouldAutorotate];
  76. }
  77. return shouldAutorotate;
  78. }
  79. /*
  80. NS_AVAILABLE_IOS(6_0)
  81. 此方法会在设备旋转且shouldAutorotate返回YES的时候才会被触发
  82. 根据对应的所有支持的取向来决定是否需要旋转
  83. 作为容器,支持的取向还决定于自己的相关子ViewControllers
  84. */
  85. - (NSUInteger)supportedInterfaceOrientations{
  86. NSUInteger supportedInterfaceOrientations = UIInterfaceOrientationMaskAll;
  87. NSArray *viewControllers = [self childViewControllersWithRotationCallbackAutoForward];
  88. for (UIViewController *viewController in viewControllers) {
  89. supportedInterfaceOrientations = supportedInterfaceOrientations & [viewController supportedInterfaceOrientations];
  90. }
  91. return supportedInterfaceOrientations;
  92. }
  93. /*
  94. NS_DEPRECATED_IOS(2_0, 6_0) 6.0以下,设备旋转时,此方法会被调用
  95. 用来决定是否要旋转
  96. */
  97. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation{
  98. BOOL shouldAutorotate = YES;
  99. NSArray *viewControllers = [self childViewControllersWithRotationCallbackAutoForward];
  100. for (UIViewController *viewController in viewControllers) {
  101. shouldAutorotate = shouldAutorotate &&  [viewController shouldAutorotateToInterfaceOrientation:toInterfaceOrientation];
  102. }
  103. return shouldAutorotate;
  104. }
  105. #pragma mark -
  106. #pragma mark 下面两个方法是在需要的情况下给基类覆盖用的,毕竟不是所有的容器都需要将相关方法传递给所有的childViewControllers
  107. - (NSArray *)childViewControllersWithAppearanceCallbackAutoForward{
  108. return self.childViewControllers;
  109. }
  110. - (NSArray *)childViewControllersWithRotationCallbackAutoForward{
  111. return self.childViewControllers;
  112. }
  113. @end

五. 创建自己的Container

设计要点

创建一个Container,首先你得设计好Container View Controller的行为和公开的API,你可以好好参考UIKit中自带的一些Container的设计风格,比如UINaivgationController就是管理着一组Content View Controller的堆栈的Container,且正在显示的是栈顶的View Controller。

主要接口有View Controller的推入,此过程中viewController会和navigationController建立父子关系,并将viewController显示出来,如果animated是YES的话,则会有过场动画:

  1. - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated

pop操作,移除栈顶的内容,会解除和navigationController的父子关系:

  1. - (UIViewController *)popViewControllerAnimated:(BOOL)animated;

当然关于pop还有一些其他的便捷接口,这里就不赘述了。

另外需要提供一些快捷的接口方便获取特定的Child View Controller,比如topViewController可以获取栈顶的View Controller。

另外如有必要,Container还需要留有delegate接口,便于通知外面Container的相关行为阶段,便于外部做出相关操作,比如UINaivgationController就会在即将要push一个新的View Controller,已经push了一个新的View Controller等时机留有delegate方法。

还有一个需要考虑的问题就是直接或者间接的Child View Controller如何快速的检索到相应的Container呢?一般Container在实现的时候就需要考虑此问题并提供相应的接口,实现的方法一般就是实现一个UIViewController的Category,比如UINavigationController,在某个View Controller中访问其navigationController属性,会向上遍历,直到找到最近的类型为UINavigationController的祖先,如果找不到则为nil:

  1. @interface UIViewController (UINavigationControllerItem)
  2. ...
  3. @property(nonatomic,readonly,retain) UINavigationController *navigationController;
  4. @end

实现一个简单的模态窗口Container

模态展示 则至少存在present,dismiss的接口,以及获取模态View Controller的属性 :

  1. #import <UIKit/UIKit.h>
  2. #import "ContainerBaseController.h"
  3. @interface SimpleModalContainerController : ContainerBaseController
  4. @property (nonatomic, readonly) UIViewController *simpleModalViewController;
  5. - (void)presentSimpleModalViewController:(UIViewController *)viewControllerToPresent
  6. animated:(BOOL)animated;
  7. - (void)dismissSimpleModalViewControllerAnimated:(BOOL)animated;
  8. @end
  9. //实现如下
  10. #import "SimpleModalContainerController.h"
  11. @interface SimpleModalContainerController ()
  12. @property (nonatomic, readwrite) UIViewController *simpleModalViewController;
  13. @property (nonatomic, strong) UIButton *backgroundButton;
  14. @end
  15. @implementation SimpleModalContainerController
  16. - (void)buttonTapped:(id)sender{
  17. [self dismissSimpleModalViewControllerAnimated:YES];
  18. }
  19. - (UIButton *)backgroundButton{
  20. if (!_backgroundButton) {
  21. _backgroundButton = [UIButton buttonWithType:UIButtonTypeCustom];
  22. _backgroundButton.backgroundColor = [UIColor blackColor];
  23. _backgroundButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  24. _backgroundButton.alpha = 0.3;
  25. [_backgroundButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
  26. }
  27. _backgroundButton.frame = self.view.bounds;
  28. return _backgroundButton;
  29. }
  30. - (void)presentSimpleModalViewController:(UIViewController *)viewControllerToPresent
  31. animated:(BOOL)animated{
  32. if (!self.simpleModalViewController && viewControllerToPresent) {
  33. self.simpleModalViewController = viewControllerToPresent;
  34. [self addChildViewController:viewControllerToPresent];
  35. [viewControllerToPresent beginAppearanceTransition:YES animated:animated];
  36. [self.view addSubview:self.backgroundButton];
  37. viewControllerToPresent.view.center = CGPointMake(CGRectGetWidth(self.view.bounds) / 2.0, CGRectGetHeight(self.view.bounds) / 2.0);
  38. [self.view addSubview:viewControllerToPresent.view];
  39. if (animated) {
  40. viewControllerToPresent.view.alpha = 0;
  41. self.backgroundButton.alpha = 0;
  42. [UIView animateWithDuration:0.3 animations:^{
  43. viewControllerToPresent.view.alpha = 1;
  44. self.backgroundButton.alpha = 0.3;
  45. } completion:^(BOOL finished) {
  46. [viewControllerToPresent endAppearanceTransition];
  47. [viewControllerToPresent didMoveToParentViewController:self];
  48. }];
  49. } else {
  50. self.backgroundButton.alpha = 0.3;
  51. [viewControllerToPresent endAppearanceTransition];
  52. [viewControllerToPresent didMoveToParentViewController:self];
  53. }
  54. }
  55. }
  56. - (void)dismissSimpleModalViewControllerAnimated:(BOOL)animated{
  57. if (self.simpleModalViewController) {
  58. [self.simpleModalViewController willMoveToParentViewController:nil];
  59. [self.simpleModalViewController beginAppearanceTransition:NO animated:animated];
  60. if (animated) {
  61. [UIView animateWithDuration:0.3 animations:^{
  62. self.backgroundButton.alpha = 0;
  63. self.simpleModalViewController.view.alpha = 0 ;
  64. } completion:^(BOOL finished) {
  65. [self.backgroundButton removeFromSuperview];
  66. [self.simpleModalViewController.view removeFromSuperview];
  67. self.simpleModalViewController.view.alpha = 1.0;
  68. [self.simpleModalViewController endAppearanceTransition];
  69. [self.simpleModalViewController removeFromParentViewController];
  70. self.simpleModalViewController = nil;
  71. }];
  72. } else {
  73. [self.backgroundButton removeFromSuperview];
  74. [self.simpleModalViewController.view removeFromSuperview];
  75. self.simpleModalViewController.view.alpha = 1.0;
  76. [self.simpleModalViewController endAppearanceTransition];
  77. [self.simpleModalViewController removeFromParentViewController];
  78. self.simpleModalViewController = nil;
  79. }
  80. }
  81. }
  82. @end

UIViewController的Category用于Child View Controller 获取上层的SimpleModalContainerController :

  1. @interface UIViewController (SimpleModalContainerController)
  2. @property (nonatomic, readonly) SimpleModalContainerController *simpleModalContainerController;
  3. @end
  4. @implementation UIViewController (SimpleModalContainerController)
  5. - (SimpleModalContainerController *)simpleModalContainerController{
  6. for (UIViewController *viewController = self.parentViewController; viewController != nil; viewController = viewController.parentViewController) {
  7. if ([viewController isKindOfClass:[SimpleModalContainerController class]]) {
  8. return (SimpleModalContainerController *)viewController;
  9. }
  10. }
  11. return nil;
  12. }
  13. @end
时间: 2024-08-19 11:41:22

Custom Container View Controller的相关文章

《iOS Human Interface Guidelines》——Container View Controller

容器视图控制器 容器视图控制器管理和展示它的子视图集合--或者子控制器集合--以一种自定义的方式.系统定义的容器视图控制器的例子有标签栏视图控制器.导航栏视图控制器和分栏视图控制器(查看Tab Bar.Navigation Bar和Split View Controller来学习更多关于这些元素的内容). API NOTE 查看UIViewController Class Reference学习关于在你的代码中定义自定义的容器视图控制器的内容. 容器视图控制器没有预定义的外观和行为. 使用容器视

Container View Controller

有时候,我们的Controler中包含有另一个controler view的view时,可以使用这种方式. https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html 把一个controller view 加入到 controler中 [self addChildViewController:conten

【IOS笔记】Creating Custom Content View Controllers

Creating Custom Content View Controllers 自定义内容视图控制器 Custom content view controllers are the heart of your app. You use them to present your app’s unique content. All apps need at least one custom content view controller. Complex apps divide the workl

【IOS笔记】View Controller Basics

View Controller Basics   视图控制器基础 Apps running on iOS–based devices have a limited amount of screen space for displaying content and therefore must be creative in how they present information to the user. Apps that have lots of information to display

M2在奋斗之ios开发--View Controller pragramming guide for IOS中文版

About View Controllers 视图控制器是应用程序数据和其视觉外形之间的一个至关重要的链接.无论何时,应用程序显示一个用户界面,其显示的内容都是由一个或一组互相合作的视图控制器管理.因此,视图控制器给你建立的应用程序提供了骨架. iOS提供了很多内置的视图控制器类来支持标准用户界面块(piece),比如导航和标签栏.作为开发应用程序的一部分,你还可以实现一个或多个自定义控制器来显示应用程序的特定内容. 概述 在模型-视图-控制器(MVC)设计模式里,视图控制器是传统的控制器对象,

组合View Controller时遇到的一点问题

View Controller的组合应用其实很常见了,比如说Tab bar controller和Navigation view controller的组合使用,像这种一般都是Navigation view controller作为Tab bar controller的一个child view controller,对应了一个Tab bar item. 然后今天在Review 另外一组的一个产品的代码时,发现他们将Tab bar controller作为了一个普通view controller的

IOS学习之table view controller、table view cell

A table view controller, like many objects, has more than one init method. There is: • initWithCoder, for view controllers that are automatically loaded from a storyboard • initWithNibName, for view controllers that you manually want to load from a n

View Controller 生命周期的各个方法的用法

loadView; This is where subclasses should create their custom view hierarchy if they aren't using a nib. Should never be called directly.这是当他们没有正在使用nib视图页面,子类将会创建自己的自定义视图层.绝不能直接调用. - (void)awakeFromNib; 这个方法用的时候,outlet还没有连接起来,是view Controller刚从storyb

View Controller容器

在 iOS 5 之前,view controller 容器是 Apple 的特权.实际上,在 view controller 编程指南中还有一段申明,指出你不应该使用它们.Apple 对 view controllers 的总的建议曾经是"一个 view controller 管理一个全屏幕的内容".这个建议后来被改为"一个 view controller 管理一个自包含的内容单元".为什么 Apple 不想让我们构建自己的 tab bar controllers