關於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矢倉大神的指點!!!下圖是他對系統控件的可視化改造!

相關文章
相關標籤/搜索