zoukankan      html  css  js  c++  java
  • ViewController容器

    在我的一个项目中,我需要实现一种容器式的 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 容器,极大地简化了我创建复杂的多分割界面的工作。

    本教程代码从这里下载

  • 相关阅读:
    leetcode教程系列——Binary Tree
    《Ranked List Loss for Deep Metric Learning》CVPR 2019
    《Domain Agnostic Learning with Disentangled Representations》ICML 2019
    Pytorch从0开始实现YOLO V3指南 part5——设计输入和输出的流程
    Pytorch从0开始实现YOLO V3指南 part4——置信度阈值和非极大值抑制
    Pytorch从0开始实现YOLO V3指南 part3——实现网络前向传播
    Pytorch从0开始实现YOLO V3指南 part2——搭建网络结构层
    Pytorch从0开始实现YOLO V3指南 part1——理解YOLO的工作
    让我佩服的人生 文章
    win8.1配置cordova+ionic等一系列东西
  • 原文地址:https://www.cnblogs.com/YH-Coding/p/5331761.html
Copyright © 2011-2022 走看看