iOS实现容器视图控制器的方法

一直以来想写一个抽屉效果,看了一些文章后发现并不是那么简单,网上的一些抽屉效果不是很严谨。看了下MMDrawerController的源码,等于定制了一个Container View Controller。(类似于系统的UINavigationController以及UITabbarController);

比如下面几个方法就是MMDrawerController实现的:

下面的描述是官方文档帮助理解什么是容器控制器,文档中以UINavigationController和UISplitViewController举例。

容器视图控制器是将来自多个视图控制器的内容合并到单个用户界面中的一种方法。容器视图控制器通常用于促进导航,并基于现有内容创建新的用户界面类型。UIKit中的容器视图控制器的例子包括UINavigationController,UITabBarController和UISplitViewController,所有这些都方便您的用户界面的不同部分之间的导航。

设计自定义容器视图控制器

几乎在任何情况下,容器视图控制器都像其他任何内容视图控制器一样管理根视图和一些内容。区别在于容器视图控制器从其他视图控制器获取其内容的一部分。它获取的内容仅限于其他视图控制器的视图,它嵌入在其自己的视图层次结构中。容器视图控制器设置任何嵌入视图的大小和位置,但原始视图控制器仍然管理这些视图内的内容。

在设计自己的容器视图控制器时,请始终了解容器和包含的视图控制器之间的关系。视图控制器的关系可以帮助告知他们的内容应该如何显示在屏幕上,以及你的容器如何在内部管理它们。在设计过程中,问自己以下问题:

1 .容器的作用是什么?它的孩子扮演什么样的角色?

2.多少个孩子同时显示?

3.兄弟视图控制器之间有什么关系(如果有的话)?

4.子视图控制器如何添加到容器或从容器中移除?

5.孩子的大小或位置能改变吗?这些变化在什么情况下发生?

6.容器是否提供任何装饰或导航相关的视图?

7.集装箱和子公司之间需要什么样的沟通?容器是否需要向小孩报告特定事件,而不是由UIViewController班级定义的标准事件?

8.容器的外观是否可以用不同的方式配置?如果是这样,怎么样?

在定义了各种对象的角色之后,容器视图控制器的实现相对简单。UIKit唯一的要求就是在容器视图控制器和任何子视图控制器之间建立正式的父子关系。亲子关系确保孩子们收到任何相关的系统消息。除此之外,大部分实际工作都发生在包含视图的布局和管理过程中,对于每个容器来说都是不同的。您可以将视图放置在容器的内容区域的任何位置,然后根据需要调整视图的大小。您还可以将自定义视图添加到视图层次结构中,以提供修饰或辅助导航。

示例:导航控制器

UINavigationController对象通过分层数据集支持导航。导航界面一次显示一个子视图控制器。界面顶部的导航栏显示数据层次结构中的当前位置,并显示后退按钮以向后移动一个级别。向下导航到数据层次结构留给子视图控制器,可以涉及使用表或按钮。

视图控制器之间的导航由导航控制器及其子节点联合管理。当用户与子视图控制器的按钮或表格行交互时,孩子要求导航控制器将新的视图控制器推入视图。孩子处理新的视图控制器的内容的配置,但导航控制器管理过渡动画。导航控制器还管理导航栏,该导航栏显示关闭最上面的视图控制器的后退按钮。

图5-1显示了导航控制器及其视图的结构。大多数内容区域由最顶层的子视图控制器填充,只有一小部分被导航栏占用。

在紧凑和常规的环境中,导航控制器一次只显示一个子视图控制器。导航控制器调整其子以适应可用空间。

示例:分割视图控制器

一个UISplitViewController对象以主 - 细节布局显示两个视图控制器的内容。在这种安排中,一个视图控制器(主视图)的内容决定了其他视图控制器显示的细节。两个视图控制器的可见性是可配置的,但也受当前环境的支配。在规则的水平环境中,分割视图控制器可以同时显示两个子视图控制器,也可以隐藏主控并根据需要显示。在紧凑的环境中,分割视图控制器一次只显示一个视图控制器。

图5-2显示了在一个常规的水平环境中的分割视图界面及其视图的结构。分割视图控制器本身只有默认的容器视图。在这个例子中,两个子视图是并排显示的。子视图的大小是可配置的,主视图的可见性也是可配置的。

在界面构建器中配置容器

要在设计时创建父子容器关系,请将容器视图对象添加到故事板场景中,如图5-3所示。容器视图对象是代表子视图控制器内容的占位符对象。使用该视图来调整和定位与容器中其他视图相关的子视图。

当您使用一个或多个容器视图加载视图控制器时,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显示了如何从容器中删除子视图控制器。willMoveToParentViewController:使用该值调用该方法nil使子视图控制器有机会为更改做准备。该removeFromParentViewController方法还调用孩子的didMoveToParentViewController:方法,传递该方法的值nil。设置父视图控制器以nil完成从容器中删除子视图。

清单5-2从容器中删除一个子视图控制器

- (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方法。例如,如果您的容器有一个由child属性引用的单个子项,那么您的容器会将这些消息转发给子项,如清单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];
}

建立一个容器视图控制器的建议

设计,开发和测试新的容器视图控制器需要时间。虽然个人行为是直截了当的,但整个控制者可能相当复杂。在实现自己的容器类时,请考虑以下提示:

只访问子视图控制器的根视图。一个容器只能访问每个孩子的根视图,也就是孩子view属性返回的视图。它不应该访问任何孩子的其他意见。

子视图控制器应该对其容器有最少的了解。子视图控制器应该关注自己的内容。如果容器允许其行为受到孩子的影响,则应该使用委托设计模式来管理这些交互。

首先使用常规视图设计您的容器。使用常规视图(而不是来自子视图控制器的视图)使您有机会在简化的环境中测试布局约束和动画过渡。当常规视图按预期工作时,将它们交换为您的子视图控制器的视图。

将控制委派给子视图控制器

容器视图控制器可以将其自身外观的某些方面委托给其一个或多个子级。您可以通过以下方式委托控制:

  1. 让一个子视图控制器确定状态栏的样式。要委派状态栏外观小孩,覆盖的一个或两个childViewControllerForStatusBarStyle,并childViewControllerForStatusBarHidden在你的容器视图控制器的方法。
  2. 让孩子指定自己喜欢的尺寸。具有灵活布局的容器可以使用孩子自己的preferredContentSize财产来帮助确定孩子的大小。