一次性厘清loadView、initFromNib、viewDidLoad之间的关系

当一个UIViewController通过代码生成之后,其事件调用顺序init → ViewDidLoad → viewWillAppear → viewDidAppear,这是众所周知的。然而在init和ViewDidLoad之间,系统还隐式调用了一些方法,今天就来探讨这部分问题。

1 问题起源

通过一个按钮跳转到控制器ApplyViewController,在ViewDidLoad中加载ApplyView.xib作为控制器的SubView,ApplyView.xib中有一个View,其类型是ApplyView,如图

图1

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ViewController.m
- (void)applyButtonClick
{
ApplyViewController *vc = [[ApplyViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}

// ApplyViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
ApplyView *applyView = [[[NSBundle mainBundle] loadNibNamed:@"ApplyView" owner:nil options:nil] lastObject];
applyView.frame = CGRectMake(0, self.navigationController.navigationBar.frame.size.height, myview.frame.size.width, myview.frame.size.height);
[self.view addSubview:applyView];
}

然后程序就崩了,提示:
'NSInternalInconsistencyException', reason: '-[UIViewController _loadViewFromNibNamed:bundle:] loaded the "ApplyView" nib but the view outlet was not set.'

我仔细检查了ApplyView.xib中的属性,发现并没有view一项,而奇怪的是,之前另一个页面完全相同的写法,就完全没有问题。

2 分析

我找到这篇文章:关于initWithNibName和loadNibNamed的区别和联系

于是尝试将xib中的File's Owner赋值为ApplyViewController,然后将Outlets中的view连线到xib中的view,再执行仍会报错,但此时是在loadNibNamed上报错:
'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key view.'

也就是说,我指定了File's Owner,所以loadNibNamed就不能用owner:nil参数了,于是我将owner改为self,又在addSubView上报错:
'NSInvalidArgumentException', reason: 'Can't add self as subview'

也就是说,此时的self.view就是xib的view!!

结论:只要是指定了xib的File's Owner,就必须给view连线,只要是连了线,就不能用loadNibNamed,而必须用initWithNibName!

1
2
3
4
5
6
7
8
9
10
11
12
// ViewController.m
- (void)applyButtonClick
{
ApplyViewController *vc = [[ApplyViewController alloc] initWithNibName:@"ApplyView" bundle:nil];
[self.navigationController pushViewController:vc animated:YES];
}

// ApplyViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
}

3 另一个问题

那么为什么在不指定xib的File's Owner的情况下,就不能用loadNibNamed加载xib呢?
一个不经意的尝试让我定位了问题的出处:在本文第3节的基础上,initWithNibName改为init,竟然成功加载了xib!!!
必然是系统在init时隐式加载了xib,参考这篇文章自定义UIViewController与xib文件的关系分析,得出以下结论:

① ViewController在init时会查找项目中与其同名(去掉Controller的部分)的xib文件,如果有,则加载为self.view

比如本文中ApplyViewController会隐式加载ApplyView.xib,不管初始化用的是init还是initWithNibName

② 加载xib是在loadView事件中进行的

③ init方法最终也会调用initWithNibName

4 最终方案

既然如此,是不是可以通过重写loadView方法不让系统隐式加载同名xib呢?
参考文章:loadView学习总结,将代码作如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ViewController.m
- (void)applyButtonClick
{
ApplyViewController *vc = [[ApplyViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}

// ApplyViewController.m
- (void)loadView
{
UIView *myView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
self.view = myView;
}

- (void)viewDidLoad
{
[super viewDidLoad];
ApplyView *applyView = [[[NSBundle mainBundle] loadNibNamed:@"ApplyView" owner:nil options:nil] lastObject];
applyView.frame = CGRectMake(0, self.navigationController.navigationBar.frame.size.height, myview.frame.size.width, myview.frame.size.height);
[self.view addSubview:applyView];
}

终于达到了想要的效果!此时的方法调用顺序:init → initWithNibName → loadView → viewDidLoad

5 延伸

苹果官方对于loadView的说明:

This is where subclasses should create their custom view hierarchy if they aren’t using a nib. Should never be called directly.

对于initWithNibName的说明:

The designated initializer. If you subclass UIViewController, you must call the super implementation of this method, even if you aren’t using a NIB. (As a convenience, the default init method will do this for you, and specify nil for both of this methods arguments.) In the specified NIB, the File’s Owner proxy should have its class set to your view controller subclass, with the view outlet connected to the main view. If you invoke this method with a nil nib name, then this class’ -loadView method will attempt to load a NIB whose name is the same as your view controller’s class. If no such NIB in fact exists then you must either call -setView: before -view is invoked, or override the -loadView method to set up your views programatically.

延伸阅读:loadView, viewDidLoad, awakeFromNib