版本
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还有很多作用, 比如说:
- 更新视图内容显示;
- 响应用户与视图的交互;
- 调整视图大小并管理整个界面布局;
- 在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, 可以分两步:
- 获取windows中 最上层的 且 正在显示 的window
- 获取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;
}
网友评论