iOS

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

2019-10-12  本文已影响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, 单例, 消息者中心通知等等.

ViewControllerB *VCB = [[ViewControllerB alloc] init];
VCB.view.backgroundColor = [UIColor purpleColor];

获取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;
}
上一篇下一篇

猜你喜欢

热点阅读