我是前言

个人很主张使用Interface Builder(以下都简称IB)来构建程序UI,包括storyboardxib,相比代码更可视和易于修改,尤其在使用AutoLayout的时候,一目了然。
但用了这么久IB之后发现一个很大的槽点,就是IB间很难嵌套混用,比如一个xib中的view是另一个xib的子view,或者一个storyboard中两个vc都用到了一个xib构建的view等。解决方法一般是代码手动拼接,这就造成了比较混乱的情况。

本文将尝试解决这个问题,实现xib的动态桥接,并提供一个支持cocoapods的开源工具类供方便使用。

一张图顶十句话:

实现效果:


黑魔法方法

实现这个功能的关键在于:在ib加载的某个时刻将placeholder的view动态替换成从xib加载的view,下面的方法就可以做到:

1
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder NS_REPLACES_RECEIVER;

这个方法很少用到,在NSObject (NSCoderMethods)中定义,由NSCoder在decode过程中调用(于-initWithCoder:之后),所以说就算从文件里decode出来的对象也会走这个方法。
方法后面有NS_REPLACES_RECEIVER这个宏:

1
#define NS_REPLACES_RECEIVER __attribute__((ns_consumes_self)) NS_RETURNS_RETAINED

在clang的文档中可以找到对这个编译器属性的介绍

One use of this attribute is declare your own init-like methods that do not follow the standard Cocoa naming conventions.

所以这个宏主要为了给编译器标识出这个方法可以像self = [super init]一样使用,并作出合理的内存管理。
So,这个方法提供了一个机会,可以将decode出来的对象替换成另一个对象

动态桥接流程

1
2
3
4
5
6
7
8
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
self = [super awakeAfterUsingCoder:aDecoder];

// 0. 判断是否要进行替换
// 1. 根据self.class从xib创建真正的view
// 2. 将placeholder的属性、autolayout等替换到真正的view上
// 3. return 真正的view
}

流程不难理解,就是有2个小难点:

  • 步骤1从xib创建真正的view时也会调用这个方法,会造成递归,如何判断
  • 迁移AutoLayoutConstrains

解决递归问题

这个topic全网可能就《这篇文章》有写,本文也是从它发起的,但是发现它的方法并不能解决所有问题(尤其是用storyboard加载xib时),所以换了个思路,采取了设置标志位的方式避免递归调用:

1
2
3
4
5
6
7
8
9
10
11
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
self = [super awakeAfterUsingCoder:aDecoder];
if (这个类的Loading标志位 -> NO)
{
Loading标志位 -> YES
从xib加载真实的View (这里会递归调用这个函数)
return 真实View
}
Loading标志位 -> NO
return self
}

方法有点土,但是有效了,源代码文章后面会给地址。

迁移AutoLayoutConstrains

由于IB在加载AutoLayoutConstrains时的顺序是先加载子View内部的约束,后加载父View上的约束,而我们替换placeholder的时机是:

  1. placehodler view被创建(只带width,height的自身约束)
  2. 真正的view被从xib动态加载(带其子view的所有约束)
  3. placeholder被替换成真的view
  4. placeholder view在其父View(一直到父父父…View)的约束被创建

所以说,迁移AutoLayout时,只需要把placeholder view的自身约束copy到真实View上就好了(停顿10s感受下)
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)replaceAutolayoutConstrainsFromView:(UIView *)placeholderView toView:(UIView *)realView
{
for (NSLayoutConstraint *constraint in placeholderView.constraints) {
NSLayoutConstraint* newConstraint = [NSLayoutConstraint constraintWithItem:realView
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:nil // Only first item
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant];
newConstraint.shouldBeArchived = constraint.shouldBeArchived;
newConstraint.priority = constraint.priority;
[realView addConstraint:newConstraint];
}
}

One more thing,保证AutoLayout生效还要加上下面这句话:

1
realView.translatesAutoresizingMaskIntoConstraints = NO;

开源项目XXNibBridge

光说方案不给源码还是不地道的,demo放到了我的github上面的XXNibBridge项目,回顾一下上面的关系图:

不得不提到IB命名约定的最佳实践方案:

将类名作为Cell或者VC的Reusable Identifier
ReuseIdentifier一直比较蛋疼,我一般将Cell的类名作为ReuseIdentifier(当然,大多数情况我们都会子类化Cell的),写法如:

1
[self.tableView registerClass:[XXSarkCell class] forCellReuseIdentifier:NSStringFromClass([XXSarkCell class])];

dequeueCell的时候同理,这样的好处在于省去了起名的恶心、通过ReuseId可以直接找到Cell类、同时重构Cell类名时ReuseId也不用去再改。

View的xib与View的类名同名 同理

实现了桥接Xib的功能的同时,也简单实现了这个命名约定:

1
2
3
4
5
// XXNibBridge.h
+ (NSString *)xx_nibID; // 类名
+ (UINib *)xx_nib; // 返回类名对应nib
+ (id)xx_loadFromNib; // 对应nib的类对象
+ (id/*UIViewController*/)xx_loadFromStoryboardNamed:(NSString *)name; // 返回类名对应的vc

所以之后的代码可以这么写:

1
[tableView registerNib:[XXSarkView xx_nib] forCellReuseIdentifier:[XXSarkView xx_nibID]];

XXNibBridge的使用

Cocoapods安装

1
pod 'XXNibBridge', :git => 'https://github.com/sunnyxx/XXNibBridge.git'

对于要支持Bridge的类,重载下面的方法:

1
2
3
4
5
6
7
#import "XXNibBridge.h"
@implementation XXDogeView
+ (BOOL)xx_shouldApplyNibBridging
{
return YES;
}
@end

在父的Xib或Storyboard中拖个UIView进来作为Placeholder,设置为真实Nib的类

保证真实Nib的类名和Nib名相同,记得在Nib中设好Class

Done.


References

http://blog.yangmeyer.de/blog/2012/07/09/an-update-on-nested-nib-loading
http://stackoverflow.com/questions/19816703/replacing-nsview-while-keeping-autolayout-constraints
http://clang-analyzer.llvm.org/annotations.html#attr_ns_consumes_self