美文网首页iOS
iOS开发之UI篇(13)—— UIViewController

iOS开发之UI篇(13)—— UIViewController

作者: 看影成痴 | 来源:发表于2019-10-12 15:14 被阅读0次

    版本
    Xcode 10.2
    iPhone 6s (iOS12.4)

    目录

    继承关系
    简介
    创建
    生命周期
    关于调用super的方法
    其他常用的方法属性
    传值
    获取currentViewController

    继承关系

    UIViewController : UIResponder : NSObject
    

    简介

    UIViewController is a generic controller base class that manages a view.
    UIViewController是一个管理视图的通用控制器基类。

    UIViewController是专门用来管理view的, 它具有但不限于在view出现或者消失时调用的方法. 从上面的继承关系中我们知道, UIViewController没有继承自UIView, 它不属于视图类, 也不显示任何内容, 没有frame这个概念. 但是UIViewController有一个view属性, 即self.view, 用于显示内容. 在一个App中, view controller并不是必要的, 但是一般我们的项目中至少包含一个子类继承自UIViewController. 比如创建模板App的时候, 系统默认创建了一个ViewController来作为window的rootViewController, 而他的self.view就是我们看到的第一个视图. (详见上一篇文章UIWindow)
    UIViewController还有很多作用, 比如说:

    1. 更新视图内容显示;
    2. 响应用户与视图的交互;
    3. 调整视图大小并管理整个界面布局;
    4. 在App中与其他对象 (包括其他视图控制器) 协调.

    创建

    每一个UIViewController都有一个view属性, 也就是经常使用到的self.view. 这个view是懒加载的, 当获取视图控制器view的时候, 首先调用view的getter方法, 这个getter中会判断是否存在view, 如果不存在, 则会调用[self loadView]方法来创建一个view. 也就是说, 每次访问UIViewController的view(比如controller.view、self.view)而且view为nil时, loadView方法就会被调用. 但本人作iOS开发这么多年, 基本上没用过loadView, 所以不打算详细讨论(此处拒绝吐槽). 毕竟人家Apple也说了:

    Should never be called directly.

    1. 使用storyboard

    新建一个子类ViewControllerA继承自UIViewController. 在storyboard中拖入一个view controller, class修改为ViewControllerA, 添加Storyboard ID 填入ViewControllerAID. 然后在需要创建的类中引入”ViewControllerA.h”, 创建代码如下:

    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    ViewControllerA *VCA = [storyboard instantiateViewControllerWithIdentifier:@"ViewControllerAID"];
    // 注: 此方式会触发initWithCoder:方法
    

    2. 使用XIB

    新建ViewControllerB继承自UIViewController, 同时勾选Also create XIB file选项, 系统会帮我们创建一个名为ViewControllerB的XIB文件并关联了ViewControllerB类. 如果没有勾选这项也可后面单独创建XIB文件, 然后在File’s Owner的class中填入ViewControllerB, 即关联到ViewControllerB类. 同样, 在需要创建的类中引入”ViewControllerB.h”, 创建代码如下:

    ViewControllerB *VCB = [[ViewControllerB alloc] initWithNibName:@"ViewControllerB" bundle:nil];
    

    3. 纯代码

    ViewControllerC *VCC = [[ViewControllerC alloc] init];
    

    生命周期

    先来看看Apple官方的一张图


    视图生命周期

    图中展示了view controller的生命周期. 但是并不包括创建/销毁, 视图layout等一些方法. 下面用一个示例来展示:
    主要代码:

    #pragma mark - 生命周期
    
    // 每次访问属性view(比如controller.view、self.view)而且view为nil时
    - (void)loadView {
        [super loadView];
        
        NSLog(@"%s", __func__);
    }
    
    
    // view加载完成
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSLog(@"%s", __func__);
        
        self.view.backgroundColor = [UIColor purpleColor];
    }
    
    
    // view准备出现. 一般用法: 改变视图方向、状态栏方向、视图显示样式等
    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];
        
        NSLog(@"%s", __func__);
    }
    
    
    // view即将布局其子视图或者其中一个view的bounds发生改变. 例如: 屏幕旋转, 添加一个sub view
    - (void)viewWillLayoutSubviews {
        
        NSLog(@"%s", __func__);
    }
    
    
    // view本身布局完成. 注意: 调用此方法时并不代表所有子视图都调整布局完成了, 每个子视图负责调整自己的布局!
    - (void)viewDidLayoutSubviews {
        
        NSLog(@"%s", __func__);
    }
    
    
    // view已经出现.
    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        
        NSLog(@"%s", __func__);
        
        // 添加一个view, 会再次调用viewWillLayoutSubviews和viewDidLayoutSubviews方法
        UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
        [self.view addSubview:view1];
    }
    
    
    // view即将消失
    - (void)viewWillDisappear:(BOOL)animated {
        
        NSLog(@"%s", __func__);
        
        [super viewWillDisappear:animated];
    }
    
    
    // view已经消失
    - (void)viewDidDisappear:(BOOL)animated {
        
        NSLog(@"%s", __func__);
        
        [super viewDidDisappear:animated];
    }
    
    
    // 当没有内存泄漏时, 正常销毁UIViewController对象会调用此方法
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
    }
    

    打印结果为:

    -[ViewControllerA loadView]
    -[ViewControllerA viewDidLoad]
    -[ViewControllerA viewWillAppear:]
    -[ViewControllerA viewWillLayoutSubviews]
    -[ViewControllerA viewDidLayoutSubviews]
    -[ViewControllerA viewDidAppear:]
    -[ViewControllerA viewWillLayoutSubviews]
    -[ViewControllerA viewDidLayoutSubviews]
    -[ViewControllerA viewWillDisappear:]
    -[ViewControllerA viewDidDisappear:]
    -[ViewControllerA dealloc]
    

    从打印结果我们可以看到, 一个生命周期中viewWillLayoutSubviews和viewDidLayoutSubviews方法可能会多次调用(当view=nil时loadView也会多次调用). 根视图(self.view)界面的布局只有在viewDidLayoutSubviews调用才确定下来, 如果我们在viewDidLoad或者viewWillAppear中add一个子视图, 其添加的视图布局可能会错乱. 笔者的解决方案是添加一个标志位, 只有第一次调用viewDidLayoutSubviews时才会add那个子视图. 当然, 如果我们使用storyboard就不会出现试图布局错乱的问题, 因为storyboard是一次性加载完所有视图控件的.

    关于调用super的方法

    在上文的实例中, 我们看到, 除了dealloc/viewWillLayoutSubviews/viewDidLayoutSubviews这三个方法外, 所有方法都调用了super的同样方法.
    为什么要调用父类的方法呢?
    因为在父类中可能作了一些初始化的操作, 如果不调用父类方法, 会导致这些初始化没有进行, 从而导致错误.
    什么时机调用父类方法呢?
    一般的, 在视图出现的过程中, 于方法的开始处调用父类的方法; 在视图消失的过程中, 于方法的结尾处调用父类方法. 写法如上文示例.

    其他常用的方法属性

    /**
     当收到内存警告时调用此方法, 此时可以停止或者取消一些耗内存的操作, 否则程序会崩溃.
     */
    - (void)didReceiveMemoryWarning;
    
    
    /**
     跳转呈现另一个视图控制器
    
     @param viewControllerToPresent 目标视图控制器
     @param flag 是否开启动画
     @param completion 完成回调
     */
    - (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion;
    
    
    
    /**
     销毁当前视图控制器
    
     @param flag 是否开启动画
     @param completion 完成回调
     */
    - (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion;
    
    
    /**
     执行segue跳转. 仅在storyboard中使用.
     一般storyboard会自动启用segues以跳转到目标控制器. 但当我们连线生成segue的时候, 如果没有从button开始连线, 而是从view controller连线,
     此时拉出来的segue并不能主动启用, 这时我们可以用代码调用这个方法来跳转到目标view controller.
    
     @param segue segue对象
     @param sender 传递的对象
     */
    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(nullable id)sender;
    
    
    /**
     通知回调方法. 仅在storyboard中使用.
     即将执行segue跳转前的通知, 用于判断是否执行该segue跳转
     
     @param identifier segue的ID
     @param sender 传递的对象
     @return YES:跳转  NO:不跳转
     */
    - (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(nullable id)sender;
    
    
    /**
     通知回调方法. 仅在storyboard中使用.
     在目标视图控制器显示之前调用此通知回调, 用于配置目标视图控制器.
     源视图控制器: segue.sourceViewController
     目标视图控制器: segue.destinationViewController
     
     @param identifier segue的ID
     @param sender 传递的对象
     */
    - (void)performSegueWithIdentifier:(NSString *)identifier sender:(nullable id)sender;
    

    传值

    view controller之间的传值可以有多种, 比如直接属性赋值, 代理, block, KVC/KVO, 单例, 消息者中心通知等等.

    • 正向传值
      正向传值, 指的是对象的持有者向对象传递值. 一般直接属性赋值, 例如, ViewControllerA持有ViewControllerB, 前者向后者传值可以是
    ViewControllerB *VCB = [[ViewControllerB alloc] init];
    VCB.view.backgroundColor = [UIColor purpleColor];
    
    • 反向传值
      反向传值, 即对象向对象的持有者传值. 一般使用代理, 当然也可使用block/通知等.

    • 两个不相干对象间传值
      如果两个对象没有直接的持有/被持有关系, 那么可以考虑使用单例或者通知等传值方式.

    获取currentViewController

    App中可以有多个window, 具体看上篇文章UIWindow. [UIApplication sharedApplication].windows中的UIWindow是按照windowLevel来排序的, level高的放在后面, 而且不管这个window的hidden是YES或NO. 另一方面, window的rootViewController并不一定是我们想要的最上层的view controller, 因为rootViewController有可能是UITabBarController或者UINavigationController, 又或者rootViewController有可能又present跳转出另一个VC.
    综上考虑, 如果想获取当前正在显示的view controller, 可以分两步:

    1. 获取windows中 最上层的 且 正在显示 的window
    2. 获取window中最上层的view controller

    具体代码如下:

    // 获取当前正在显示的view controller
    - (UIViewController *)getCurrentViewController {
    
        // 获取windows中 最上层的 且 正在显示 的window
        UIWindow *topWindow = nil;
        NSArray<UIWindow *> *windows = [[UIApplication sharedApplication].windows copy];
        for (int i=(int)windows.count-1; i>=0; i--) {
            UIWindow *tempWindow = windows[i];
            if (tempWindow.hidden == NO) {
                topWindow = tempWindow;
                break;
            }
        }
        
        // 获取该window中最上层的view controller
        UIViewController *result = topWindow.rootViewController;
        while (result.presentedViewController) {
            result = result.presentedViewController;
        }
        if ([result isKindOfClass:[UITabBarController class]]) {
            result = [(UITabBarController *)result selectedViewController];
        }
        if ([result isKindOfClass:[UINavigationController class]]) {
            result = [(UINavigationController *)result visibleViewController];
        }
    
        return result;
    }
    

    相关文章

      网友评论

        本文标题:iOS开发之UI篇(13)—— UIViewController

        本文链接:https://www.haomeiwen.com/subject/imfoectx.html