前言
大家都知道,创建Controller时同时创建xib文件,系统会自动帮我们"关联"上xib文件。这个"关联"是在创建时发生的吗,我删除xib文件后单独创建一个同名的xib还有效吗?
这篇文章最终讲述的是,UIViewController默认初始化方法与xib文件命名的关系,换句话说就是,系统匹配UIViewController对应的xib文件的规则。
有兴趣的同学可以看看我是如何碰到和解决这个问题的,赶时间的同学可以直接看最终结论。
iOS xib文件命名的坑
今天碰到了一个很诡异的崩溃问题,后来发现是xib命名惹的祸。系统本想帮我们偷懒,结果却给我们挖了个坑。
先看看下面这个崩溃
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-[UIViewController _loadViewFromNibNamed:bundle:] loaded the "DetailView" nib but the view outlet was not set.'
这个崩溃提示很常见,无非就是xib的File's Owner的view忘记连线了,连上就好了,像下图这样。

但是我的问题没这么简单,先描述一下我的场景。
我有一个详情页面DetailViewController,上方是详情内容,下方是评论列表,我用tableView来实现这个页面,我创建了一个DetailView.xib来绘制上方的详情内容部分,然后将xib创建的DetailView赋值给tableView.tableViewHeader,这个控制器本身是没有关联xib的。代码如下。
//DetailViewController.m
UIView *detailView = [[[NSBundle mainBundle] loadNibNamed:@"DetailView" owner:nil options:nil] firstObject];
_tableView.tableHeaderView = detailView;
[self.view addSubview:_tableView];
创建DetailViewController文件时没有创建同名的xib的,self.view使用运行时系统默认创建的。DetailView.xib也是单独创建的,File's Owner的Custom Class没有赋值,File's Owner的view属性都不存在,根本不存在连线这一说。怎么就崩溃提示说DetailView的view没有连线呢?

我们看一下控制台完整的崩溃日志
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-[UIViewController _loadViewFromNibNamed:bundle:] loaded the "DetailView" nib but the view outlet was not set.'
*** First throw call stack:
(
0 CoreFoundation 0x00000001104ca6fb __exceptionPreprocess + 331
1 libobjc.A.dylib 0x000000010fa6eac5 objc_exception_throw + 48
2 CoreFoundation 0x00000001104ca555 +[NSException raise:format:] + 197
3 UIKitCore 0x0000000112c354dc -[UIViewController _loadViewFromNibNamed:bundle:] + 683
4 UIKitCore 0x0000000112c35d39 -[UIViewController loadView] + 177
5 UIKitCore 0x0000000112c36048 -[UIViewController loadViewIfRequired] + 172
6 UIKitCore 0x0000000112c36868 -[UIViewController view] + 27
7 UIKitCore 0x000000011326ec33 -[UIWindow addRootViewControllerViewIfPossible] + 122
8 UIKitCore 0x000000011326f327 -[UIWindow _setHidden:forced:] + 289
9 UIKit 0x000000012c590bbd -[UIWindowAccessibility _orderFrontWithoutMakingKey] + 86
10 UIKitCore 0x0000000113281f86 -[UIWindow makeKeyAndVisible] + 42
11 ViewControllerXibText 0x000000010f1976f2 -[AppDelegate application:didFinishLaunchingWithOptions:] + 434
从日志中可以看出,崩溃到UIViewController的初始化方法了,我恍然大悟,难道[[DetailViewController alloc] init]时系统自动调用DetailView.xib去创建view去了?经过我的测试验证了我的猜测。
以前我只知道,如果你用默认初始化方法去创建UIViewController,系统会匹配同名的xib文件作为self.view。不过DetailView.xib并没有和DetailViewController.xib同名,为什么系统会匹配到DetailView.xib呢?系统匹配UIViewController对应的xib文件的规则到底是怎么样的?
众所周知,UIViewController的默认初始化方法是
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
即使你调用 [[UIViewController alloc] init],最终也会调用到上面这个方法里。
这个方法的官方注释里有这么一句话
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.
如果nibName参数传nil,系统会尝试加载和controller名字"相近"的xib。原来症结就在"相近"这个词这里了啊。怎么才算相近呢?
最终结论
插一脚,先回答一下前言中提出的问题吧。
实际上,我们在创建Controller文件时勾选同时创建xib文件,这里系统确实做了一点关联,但并并不是你想象中的关联,系统只是创建了同名的xib文件,并把Custom Class设置成你创建的类名,把File'Owner的view连线。最终Controller加载哪个xib文件是在运行时系统的-loadView方法里决定的,-loadView方法里加载哪个xib文件又和xib文件的命名有关,具体规则请继续往下看。
最终,我在官方文档的UIViewController的nibName属性中找到了系统匹配的规则(藏的挺深啊),看下图。

1.如果类名以"Controller"结尾,系统会去匹配去掉"Controller"这个单词后剩余的名字,比如 MyViewController,系统会去匹配 MyView.xib。
2.系统会匹配和控制器同名的xib,比如 MyViewController,系统会去匹配 MyViewController.xib。
3.我还留意到上图中Note中提到的,通过xib文件名加后缀,~iphone ~ipad,还可以分别加载iPhone和iPad的xib,这点对Universal工程应该特别有用。
4.如果1.和2.同时存在,以哪个规则为准呢?我还真试了下,在iOS12下2.优先生效,即优先匹配同名的xib,但是iOS12以下的系统可能情况不一样,因为之前我有看到简书里遇到类似问题的人,从他们的情况来看,真机和模拟器表现不一样,而且还是1.优先生效。
如何打破系统xib匹配规则呢
1.修改xib文件名
例如 MyViewController.xib修改为 MyViewController_x.xib,或 MyView.xib修改为 MyView_x.xib。
2.调用-initWithNibName:bundle:方法时指定具体的xib文件名
DetailViewController *detailViewController = [[DetailViewController alloc] initWithNibName:@"MyView" bundle:nil];
3.重写UIViewController的-loadView方法
-initWithNibName:bundle:方法的注释里有提到,如果nibName传nil,系统的-loadView方法会尝试匹配名字"相似"的xib文件作为self.view初始化。那我们重写-loadView方法就可以"禁用"系统的自动匹配了。
@implementation DetailViewController
- (void)loadView
{
self.view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 1)]; //这里self.view宽高设置多少没关系,系统在-loadView后会在此设置self.view的frame
}
- (void)viewDidLoad {
[super viewDidLoad];
}
@end
如果觉得这篇文章对你有帮助,请点个赞吧。如果有疑问可以关注我的公众号我留言。
转载请注明出处,谢谢!
