原文: http://www.cocoanetics.com/2012/04/containing-viewcontrollers/
在我的一个项目中,我需要实现一种容器式的 view controller。我感觉几乎是寸步难行,因为这种技术用的人是那么的少。因为很显然,开发者更喜欢重用和利用已有的view controller,而不是发明新的容器。
但是在某些情况下你更需要定制自己的容器。比起UINavigationController 和 UITabBarController,自己的容器更能简化你的代码。想起你什么时候以及什么情况下会使用这两个控制器吗?
我很容易就想到一个例子。当你想用 view controller 去包含多个占据全窗口的view 时,可以用一个隐藏掉导航栏的 UINavigationController。如果标准的转换动画不能满足你,你还可能要自己定义视图切换和动画。不幸的是,我们今天不想讨论这个,而是要讨论如何实现自己的容器。
首先,你需要对 view 以及 view controllers 的树形结构有一个了解。在iOS 5 以前,我们经常构建一个 view controller 对象,然后将它的view添加到已有的视图树当中。现在不需要了!
现在,你再也不会这样做了。相反,你会用 UIViewController 来添加、删除子viewcontroller 。
另外, 我们已经习惯于把 view controller 看做是整个屏幕,例如tab bar controller 中的子视图控制器。但 UISplitViewController 的出现,则打破了这个铁律。Viewcontrollers仅管理了屏幕当中的一部分区域——当然,对于屏幕空间更加宝贵的 iPhone 来说,则是整个屏幕,除了在屏幕的边沿会有一个 content bar。UISplirViewController有2个子控制器集合,一个针对左边(“主视图”),另一个则针对右边(“详细视图”)。
UIViewController 有两个方法,用于添加和删除一个子视图控制器。
它们属于 UIViewController 的新类别“UIContainerViewControllerProtectedMethods”:
@interface UIViewController (UIContainerViewControllerProtectedMethods) - (void)addChildViewController:(UIViewController *)childController; - (void)removeFromParentViewController; @end |
这两个方法的作用正如其名。你可能猜到如何用它们了。正如你使用addSubview 和 removeFromSuperview 一样。后面我们会演示。注意:我们假设你使用 ARC。
根据文档,我们可以自由定义使用方式。比如一次只能见到一个 VC(类似导航控制器),或者通过tab 进行导航(tab bar 控制器),或者多个 VC 按一定顺序排在一起(UIPageontrolle)。
你可以向子控制器集合中干3件事,它们有少许不同:
你必须确认这 4 个委托方法能被正确调用,额外的两个方法在 VC 被添加到一个新的父容器之前和之后调用。当parent 为 nil,则表明 VC 从容器中删除。
为什么要关心委托消息的发生?因为我们经常会使用view(Did|Will)(A|Disa)pear 方法来初始化以及销毁某些东西,因此必须关心这些方法调用后的结果。如果这些方法执行错误,你会在控制台中看到一些讨厌的“unbalanced messages”警告。我们用一个例子进行说明。假设你想达到某种类似于 tab bar controller 的效果。我们有一个view controller 数组,我们要在这些 view controller 之间进行切换。由于作为容器的 VC 是 app 的rootViewController,当 app 启动时,我们要显示第一个子控制器。
准备
在实现部分,我们声明一些变量(我们只想在ContainerViewController 中访问它):
@implementation ContainerViewController { NSArray *_subViewControllers; UIViewController *_selectedViewController; UIView *_containerView; } |
你可以将 subViewControllers 设成一个静态的 ViewController 数组。selectedViewController 会指向当前正在显示的 VC,containerView是一个容器,代表子VC 将放到 containerViewController 的某一个区域。然后在 loadView:方法中:
- (void)loadView { // 构建 VC 视图 CGRect frame = [[UIScreen mainScreen] applicationFrame]; UIView *view = [[UIView alloc] initWithFrame:frame]; view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; view.backgroundColor = [UIColor blueColor]; // 在 view 基础上构建 content view (高度缩减100) frame = CGRectInset(view.bounds, 0, 100); _containerView = [[UIView alloc] initWithFrame:frame]; _containerView.backgroundColor = [UIColor redColor]; _containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [view addSubview:_containerView]; // 这里container VC 会自动调整方向 self.view = view; } |
为便于区分,view 的背景色为蓝色,container view 的背景色为红色。子 VC 会在红色区域显示,当我们旋转设备,子 VC 会自动调整大小。
app delegate 中的代码缺少新意,加入 import 语句然后创建 ContainerViewController 并设置为 RootVC。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; ContainerViewController *container = [[ContainerViewController alloc] init]; self.window.rootViewController = container; [self.window makeKeyAndVisible]; return YES; } |
接下来需要用几个 view controller 来扮演子 VC 的角色。我创建了一个简单的ViewController子类,上面仅有一个 UILabel 用于显示某些文本。为简便起见,我们显示的是它们的 description。
- (void)loadView { // set up the base view CGRect frame = [[UIScreen mainScreen] applicationFrame]; UILabel *label = [[UILabel alloc] initWithFrame:frame]; label.numberOfLines = 0; // multiline label.textAlignment = UITextAlignmentCenter; // let's just have this view description label.text = [self description]; self.view = label; } |
|
你以前肯定没见过只有一个 UILabel 构成的 view controller。
添加
接下来我们要将这些 view controller 放到数组中并将数组加到容器中。在app delegate 中加入以下内容:
// make an array of 5 PageVCs NSMutableArray *tmpArray = [NSMutableArray array]; for (int i=0; i<5; i++) { PageViewController *page = [[PageViewController alloc] init]; [tmpArray addObject:page]; } // set these as sub VCs [container setSubViewControllers:tmpArray]; |
重载 setSubViewControllers 方法,以便选择第一个VC(索引0)作为 selected VC并显示。当然,我们无法在 setter 方法中真的去显示 VC,因为 view 还未加载,同时我们的containerView 变量仍然还是 nil。
- (void)setSubViewControllers:(NSArray *)subViewControllers { _subViewControllers = [subViewControllers copy]; if (_selectedViewController) { // TODO: remove previous VC } _selectedViewController = [subViewControllers objectAtIndex:0]; // cannot add here because the view might not have been loaded yet } @synthesize subViewControllers = _subViewControllers; |
相反,我们应该在 viewWillAppear 中显示 VC,因为loadView 方法已经得到调用。另外,如果我们发现 selected VC 的 parent 已经是 self,我们可以什么都不做,已避免一些不必要的动作。
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (_selectedViewController.parentViewController == self) { // nowthing to do return; } // adjust the frame to fit in the container view _selectedViewController.view.frame = _containerView.bounds; // make sure that it resizes on rotation automatically _selectedViewController.view.autoresizingMask = _containerView.autoresizingMask; // add as child VC [self addChildViewController:_selectedViewController]; // add it to container view, calls willMoveToParentViewController for us [_containerView addSubview:_selectedViewController.view]; // notify it that move is done [_selectedViewController didMoveToParentViewController:self]; } |
调用顺序为 viewWillAppear,viewDidAppear, willMoveToParentViewController and didMoveToParentViewController。注意,除了最后一个外,其他方法都是被自动调用的。由于未知原因 didMove 方法不会自动调用,因此我们必须手动调用。
接下来,我们需要从一个 VC 跳到下一个 VC。
转换
要在子控制器之间切换,我们需要增加一个手势识别器。朝左扫动,将控制器向前切换一页,朝右扫动则向后切换一页。在 loadView 中加入:
// add gesture support UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft:)]; swipeLeft.direction = UISwipeGestureRecognizerDirectionLeft; [view addGestureRecognizer:swipeLeft]; UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight:)]; swipeRight.direction = UISwipeGestureRecognizerDirectionRight; [view addGestureRecognizer:swipeRight]; |
swipeLeft 和 swipeRight 方法实现如下。为了简单起见,我们用两个手势识别器。因为要在一个手势识别器中识别两个方向比较麻烦。
- (void)swipeLeft:(UISwipeGestureRecognizer *)gesture { if (gesture.state == UIGestureRecognizerStateRecognized) { NSInteger index = [_subViewControllers indexOfObject:_selectedViewController]; index = MIN(index+1, [_subViewControllers count]-1); UIViewController *newSubViewController = [_subViewControllers objectAtIndex:index]; [self transitionFromViewController:_selectedViewController toViewController:newSubViewController]; } } - (void)swipeRight:(UISwipeGestureRecognizer *)gesture { if (gesture.state == UIGestureRecognizerStateRecognized) { NSInteger index = [_subViewControllers indexOfObject:_selectedViewController]; index = MAX(index-1, 0); UIViewController *newSubViewController = [_subViewControllers objectAtIndex:index]; [self transitionFromViewController:_selectedViewController toViewController:newSubViewController]; } } |
从一个 VC 跳转到另一个 VC 用transitionFromViewController:toViewController:方法来实现。这是真正有意思的地方。用一个巧妙的方法处理最麻烦的视图添加和删除工作。当然,一些附带的消息传送是必须的。
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController { if (fromViewController == toViewController) { // cannot transition to same return; } // animation setup toViewController.view.frame = _containerView.bounds; toViewController.view.autoresizingMask = _containerView.autoresizingMask; // notify [fromViewController willMoveToParentViewController:nil]; [self addChildViewController:toViewController]; // transition [self transitionFromViewController:fromViewController toViewController:toViewController duration:1.0 options:UIViewAnimationOptionTransitionCurlDown animations:^{} completion:^(BOOL finished){ [toViewController didMoveToParentViewController:self]; [fromViewController removeFromParentViewController]; }]; } |
有许多 UIViewAnimationOptionTransition变量,但你没必要关心它。如果你想让两个 view 执行动画块,也可以将该选项指定为0。
之前我想用以前的方式去执行动画。但这会有一些我们意想不到的后果。你需要在转换之前调用“will”委托方法,而在转换之后调用“did”委托方法。如果你自己执行动画,iOS 5 将自动为你发送这些消息,但它会同时发送这些消息。这导致无法在VC显示和消失时执行不同的动作。
结束语
为了让所有的消息被调用并保持平衡,花了我不少的时间。
这个示例程序最终得以正常运行。
一旦你掌握了本文中的两个技术,在通向自己实现 viewcontroller容器的路上,将迈出你前所未有的一步。
有一件事情,我至始至终都没有提到,为什么在转换动画中,新控制器的view总是会加在容器view的主视图上。这简化了某些工作,因为知道在动画在哪个阶段来添加或者删除某些视图是没有必要的。
但是会有这种情况,你不想让动画在整个 container VC 的区域上执行。
对于这种情况,我所能想到的就是另外用一个子视图遮住这部分。或者可以遍历 viewcontrollers,然后让其中一个遮住 container view 之外的区域。然后裁剪它的 subviews 仅仅留下所需的部分。
各种 view controller 容器的最大好处是旋屏消息(should|will|did)方法可以传递到你的VC 树的最末梢。除非你关闭了它,也就是覆盖 automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers方法,返回 NO。
一旦那些我们曾经期待已久的 API 变成过往的时候,谁还会想那么多呢?使用view controller 容器,极大地简化了我创建复杂的多分割界面的工作。
本教程代码从这里下载。