我的很主张使用Interface Builder
(如下都简称IB
)来构建程序UI,包括storyboard
和xib
,相比代码更可视和易于修改,尤为在使用AutoLayout的时候,一目了然。
但用了这么久IB以后发现一个很大的槽点,就是IB间很难嵌套混用
,好比一个xib中的view是另外一个xib的子view,或者一个storyboard中两个vc都用到了一个xib构建的view等。解决方法通常是代码手动拼接,这就形成了比较混乱的状况。
本文将尝试解决这个问题,实现xib的动态桥接
,并提供一个支持cocoapods
的开源工具类供方便使用。html
实现这个功能的关键在于:在ib加载的某个时刻将placeholder
的view动态替换成从xib加载的view,下面的方法就能够作到:ios
1 |
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder NS_REPLACES_RECEIVER; |
这个方法不多用到,在NSObject (NSCoderMethods)
中定义,由NSCoder
在decode过程当中调用(于-initWithCoder:
以后),因此说就算从文件里decode出来的对象也会走这个方法。
方法后面有NS_REPLACES_RECEIVER
这个宏:git
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出来的对象替换成另外一个对象
动态桥接流程
github
1 2 3 4 5 6 7 8 9 |
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder
{
self = [super awakeAfterUsingCoder:aDecoder]; // 0. 判断是否要进行替换 // 1. 根据self.class从xib建立真正的view // 2. 将placeholder的属性、autolayout等替换到真正的view上 // 3. return 真正的view } |
流程不难理解,就是有2个小难点:函数
递归
,如何判断AutoLayoutConstrains
这个topic全网可能就《这篇文章》有写,本文也是从它发起的,可是发现它的方法并不能解决全部问题(尤为是用storyboard加载xib时),因此换了个思路,采起了设置标志位的方式避免递归调用:工具
1 2 3 4 5 6 7 8 9 10 11 12 |
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder
{
self = [super awakeAfterUsingCoder:aDecoder]; if (这个类的Loading标志位 -> NO) { Loading标志位 -> YES 从xib加载真实的View (这里会递归调用这个函数) return 真实View } Loading标志位 -> NO return self } |
方法有点土,可是有效了,源代码文章后面会给地址。ui
因为IB在加载AutoLayoutConstrains时的顺序是先加载子View内部的约束,后加载父View上的约束,而咱们替换placeholder的时机是:this
因此说,迁移AutoLayout时,只须要把placeholder view的自身约束copy到真实View上就行了
(停顿10s感觉下)
代码以下:spa
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (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生效还要加上下面这句话:
code
1 |
realView.translatesAutoresizingMaskIntoConstraints = NO; |
光说方案不给源码仍是不地道的,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]]; |
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.
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
原创文章,转载请注明源地址,blog.sunnyxx.com