系统是如何匹配Controller对应的xib文件的

1,927 阅读5分钟

前言

大家都知道,创建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没有连线呢?

DetailView.xib

我们看一下控制台完整的崩溃日志

*** 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属性中找到了系统匹配的规则(藏的挺深啊),看下图。

xib匹配规则

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

如果觉得这篇文章对你有帮助,请点个赞吧。如果有疑问可以关注我的公众号我留言。
转载请注明出处,谢谢!