关于IB_DESIGNABLE / IBInspectable的那些须要注意的事

前言

IB_DESIGNABLE / IBInspectable 这两个关键字是在WWDC 2014年"What's New in Interface Builder"这个Session里面,用Swift讲过一个例子。也是随着Xcode 6 新加入的关键字。html

这两个关键字是用在咱们自定义View上的,目前暂时只能用在UIView的子类中因此系统自带的原生的那些控件使用这个关键字都没有效果。ios

Live RenderingYou can use two different attributes—@IBDesignable and @IBInspectable—to enable live, interactive custom view design in Interface Builder. When you create a custom view that inherits from the UIView class or the NSView class, you can add the @IBDesignable attribute just before the class declaration. After you add the custom view to Interface Builder (by setting the custom class of the view in the inspector pane), Interface Builder renders your view in the canvas.You can also add the @IBInspectable attribute to properties with types compatible with user defined runtime attributes. After you add your custom view to Interface Builder, you can edit these properties in the inspector.git

其大意就是说,“所见即所得”的思想,咱们能够将自定义的代码实时渲染到Interface Builder中。而它们之间的桥梁就是经过两个指令来完成,即@IBDesignable和@IBInspectable。咱们经过@IBDesignable告诉Interface Builder这个类能够实时渲染到界面中,不管咱们drawRect里面多么复杂,自定义有多复杂,Xib / Storyboard均可以把它编译出来,而且渲染展现出来。可是这个类必须是UIView或者NSView的子类。经过@IBInspectable能够定义动态属性,便可在Attributes inspector面板中可视化修改属性值。github

@IBInspectable var integer: Int = 0
 @IBInspectable var float: CGFloat = 0
 @IBInspectable var double: Double = 0
 @IBInspectable var point: CGPoint = CGPointZero
 @IBInspectable var size: CGSize = CGSizeZero
 @IBInspectable var customFrame: CGRect = CGRectZero
 @IBInspectable var color: UIColor = UIColor.clearColor()
 @IBInspectable var string: String = ""
 @IBInspectable var bool: Bool = false复制代码

这两个关键字不是今天的重点,看个Demo就会使用了。 Demo地址canvas

若是想看Session的话,能够看这两个WWDC 2014的连接 whats_new_in_xcode_6 whats_new_in_interface_builder 苹果官方文档xcode

今天来分享一下我使用这两个关键字的时候遇到的一些问题和解决过程。app

1.The agent raised a "NSInternalInconsistencyException" exception

file://BottomCommentView-master/BottomCommentView/Base.lproj/Main.storyboard: error: 
IB Designables: Failed to update auto layout status: The agent raised a "NSInternalInconsistencyException" exception: Could not load NIB in bundle: 'NSBundle </Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Overlays> (loaded)' with name 'BottomCommentView'

file://BottomCommentView/Base.lproj/Main.storyboard: error:
 IB Designables: Failed to render instance of BottomCommentView: The agent threw an exception.复制代码

咱们会看到面板上Designables这里显示的是一个Crashed,Xib / Storyboard 竟然也会Crashed!整个app是跑起来了,可是报了2个错,不能忍!这两个错实际上是编译时候Xib报的错误,并非运行时的错误。 ide

当咱们看到Debug的时候,确定第一想到的就是点Debug。可是很不幸的是,在这种状况下,点击Debug,每次都会告诉你“Finishing debugging instance of XXXX for interface Builder”,即便你在你自定义的View里面打了断点,也无济于事。ui

回到问题上来,咱们来仔细看看崩溃信息。信息上说Could not load NIB in bundle,而且还给了咱们一个相似地址同样的东西'NSBundle (loaded)',咱们能够定位到时Xib在从bundle中读取出来出错了。spa

经过在网上查找资料,问题实际上是这样的。

When loading the nib, we're relying on the fact that passing bundle: nil defaults to your app's mainBundle at run time.

每次咱们取mainBundle的时候,都是用的默认的方法

let nib = UINib(nibName: String(StripyView), bundle: nil)复制代码

这里在Xib / Storyboard 编译的时候,咱们须要告诉iOS系统,咱们要指定哪个bundle类去读取。把上面的代码改为下面这样就能够了。

let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: String(StripyView), bundle: bundle)复制代码

或者这样

#if TARGET_INTERFACE_BUILDER
        NSBundle *bundle = [NSBundle bundleForClass:[self class]];
        [bundle loadNibNamed:@"BottomCommentView" owner:self options:nil];
#else
        [[NSBundle mainBundle] loadNibNamed:@"BottomCommentView" owner:self options:nil];

#endif复制代码

Ps:若是你自定义的View不显示在Xib / Storyboard上,可是程序一运行就又能显示出View来,缘由也有多是这个缘由,虽然Xib / Storyboard没有报错,由于app没有运行起来,Xib / Storyboard并不知道上下文,因此没有把咱们自定义的View加载出来。

2.代码或者Xib依旧不显示自定义控件的样子

若是你按照上面的第一个问题里面加上了bundle的代码以后仍是不显示,那多是你代码加的地方不对。

若是是代码手动建立控件的话,会调用initWithFrame方法

- (instancetype)initWithFrame:(CGRect)frame复制代码

若是是经过Xib / Storyboard 拖拽显示控件的话,会调用initWithCoder方法

- (instancetype)initWithCoder:(NSCoder *)aDecoder复制代码

须要在对应的这两个方法里面去加上bundle的方法。若是为了保险起见,那这两个init方法里面都加上问题一里面的代码吧。

3.Failed to update auto layout status: The agent crashed / Failed to render instance of XXXXXXX: The agent crashed

file://BottomCommentView/Base.lproj/Main.storyboard: error: 
IB Designables: Failed to update auto layout status: The agent crashed

file://BottomCommentView/Base.lproj/Main.storyboard: error: 
IB Designables: Failed to render instance of BottomCommentView: The agent crashed复制代码

若是是遇到了这个问题,是比较严重的,这个问题不像问题一,问题一整个app是能够运行的,错误来源于Xib / Storyboard编译时候的错误,可是并不影响这个app的运行。

可是这个问题会直接致使整个app闪退,直接Crashed掉!没办法,咱们只能打断点debug一下。

若是你在Designables 那里把Debug打开,而后断点打到initWithCoder 和 initWithFrame那里,会发现程序老是运行到这一行

self = [super initWithCoder:aDecoder];复制代码

或者这一行

self = [super initWithFrame:frame];复制代码

就崩溃了。其实从下面的栈信息也能够很快看出发生了什么:

能够很明显的看到,是initWithCoder这个方法陷入了死循环。因为这个死循环致使了程序Crashed了。

但是这里为何会死循环呢?其实根本缘由在于,咱们自定义的类的class写成本身了。

来看看到底发生了什么。如今在Xode 7中,咱们默认建立一个View,是不给咱们默认生成一个XIB文件,ViewController会有下面那个选项,能够选择勾上。

在咱们建立完这个类的时候,咱们还要再建立一个Xib和这个类进行关联。

再对比一下咱们建立TableviewCell的过程

通常咱们会勾选上那个“Also create XIB file”,建立完成以后,咱们就会在Custom Class里面把咱们这个cell的类名填上。

若是咱们如今自定义View的时候也是相同作法,建立完Xib文件以后,File‘s owner关联好了以后。而后在Custom Class里面填上了咱们自定义的类以后,这个时候就错了!

为何咱们平时相同的作法,到这里就错误了呢?

咱们来考虑一下咱们自定义View加载的过程。咱们这个自定义View确定是放在了一个ViewController上面,代码建立出来或者直接拖拽到Xib / Storyboard 上。用代码或者SB上面拖一个View,这个时候咱们须要指定这个类是什么,这个毋庸置疑,是绝对没有问题的。SB上面拖的View的class确定要选择咱们自定义的这个View。

可是在加载咱们这个View的时候,会走initWithCoder / initWithFrame 方法,在这里方法里面又会去调用super的这个方法,如今咱们把这个class写成了本身,依照咱们上面调试的log,能够看到,initWithCoder之后,会按照如下的路线去调用.

[NSBundle loadNibName] —— [UINib instantiateWithOwner:options] ——[UINibDecoder decodeObjectForKey:]——UINibDecoderDecodeObjectForValue——[UIRuntimeConnection initWithCoder]——[UINibDecoder decodeObjectForKey:]——UINibDecoderDecodeObjectForValue——[UIClassSwapper initWithCoder:]——[BottomCommentView initWithCoder:]

从NSBundle加载开始,解析完以后会调用到ClassSwapper 的initWithCoder,因为咱们class写了本身,这里就陷入死循环了。程序崩溃!这里就跟set方法里面调用点语法赋值同样,无限的递归调用了。

通过上面的分析以后,咱们就知道了问题就出在咱们在initWithCoder里面又调用了loadNibName,loadNibName又会去最终调UIClassSwapper initWithCoder。难道是咱们custom class不对么?对比一下咱们自定义tableViewCell的class就是自己,怎么就没有这个问题呢。

咱们来仔细看看tableViewCell咱们是怎么加载的,咱们的Xib的class仍是本身,可是registerWithNibName的方法调用在tableView中,这样就不会无限递归了。

这里固然咱们也能够仿照这个方法作,那咱们须要把loadNibName写到另一个类中去。class仍是写本身自己,用那个类来加载咱们这个View,这样就能够不崩溃,不会无限递归了。可是问题又来了,咱们没法在Xib/Storyboard上实时预览到咱们的View了。

这里须要提一下IB_DESIGNABLE的工做原理。当咱们用了IB_DESIGNABLE关键字之后,Xib/StoryBoard会在不运行整个程序的状况下,把这个View代码编译跑一遍,因为没有程序上下文,全部的编译就只在这个view的代码中进行。

咱们在ViewController里面拖拽了一个View,而且更改它的class为咱们自定义的class,那么接下来全部view的绘制都会交给咱们这个自定义view的class,由这个class来管理。这里就分两种状况了。第一种状况就是我文章一开头给的Demo的例子,用DrawRect代码绘制出这个View的样子。这里不会出现任何问题。第二种状况就是咱们还想用一个Xib来显示View,这种状况就是Xib/StoryBoard里面再次加载Xib的状况了。因为如今咱们自定义的class有了接管整个view的绘制权利,那么咱们就应该在initWithCoder中loadNibName,把整个View在初始化的时候load出来。根据上面的分析,咱们找到崩溃的缘由是无限递归,这里又必需要调用initWithCoder,咱们的惟一办法就是把class改为父类的class,即UIView,这时候一切就行了,Xib/Storyboard不报错,也能及时显示出view的样子来了。

总结一下:

when using loadNibNamed:owner:options:, the File's Owner should be NSObject, the main view should be your class type, and all outlets should be hooked up to the view, not the File's Owner.

Ps.这里说的仅仅是loadNibNamed而不是initWithNibName。顺带提一下他们俩的不一样点。initWithNibName要加载的Xib的类为咱们定义的ViewController。loadNibNamed要加载的Xib的类为NSOjbect。他们的加载方式也不一样,initWithNibName方法:是延迟加载,这个View上的控件是 nil 的,只有到须要显示时,才会不是 nil。loadNibNamed是当即加载,调用这个方法加载的xib对象中的各个元素都已经存在。

总结

当我第一次知道IB_DESIGNABLE / IBInspectable以后,感受到特别的神奇,连咱们自定义化的View也能够及时可见了。不过通过一段研究之后就发现。IB_DESIGNABLE / IBInspectable仍是有一些缺陷的。IB_DESIGNABLE暂时只能在UIView的子类中用,经常使用的UIButton加圆角这些暂时也无法预览。IBInspectable实质是在Runtime Attributes设置了值,这也使得IBInspectable只能使用经常使用类型。NSDate这种类型无法设置成IBInspectable。

以上就是我和你们分享的IB_DESIGNABLE / IBInspectable使用过程当中遇到的一些“坑”。欢迎你们和在微博上和我多多交流@halfrost

更新:

下面这一段要感谢@Andy矢倉 微博上面指点我,其实系统的子类能够这么作:抽了几个经常使用的控件的公共类,顺便用External剥离经常使用属性,更复杂的移步这个库IBAnimatable

@Andy矢倉还提醒说,用这个特性最好是iOS8 + Swift,OC或者iOS7都会出现Failed to update并且无解,再次感谢@Andy矢倉大神的指点!!!下图是他对系统控件的可视化改造!

相关文章
相关标签/搜索