原文: 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件事,它们有少许不同:

  • Add 添加到容器中
  • Remove 从容器中删除
  • Transition 切换到另一个控制器 (例如:加入一个新的,删除一个旧的)

你必须确认这 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 容器,极大地简化了我创建复杂的多分割界面的工作。

本教程代码从这里下载


Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐