前言
IB_DESIGNABLE / IBInspectable 這兩個關(guān)鍵字是在WWDC 2014年"What's New in Interface Builder"這個Session里面怔檩,用Swift講過一個例子。也是隨著Xcode 6 新加入的關(guān)鍵字则北。
這兩個關(guān)鍵字是用在我們自定義View上的就漾,目前暫時只能用在UIView的子類中所以系統(tǒng)自帶的原生的那些控件使用這個關(guān)鍵字都沒有效果呐能。
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.
其大意就是說,“所見即所得”的思想抑堡,我們可以將自定義的代碼實時渲染到Interface Builder中摆出。而它們之間的橋梁就是通過兩個指令來完成,即@IBDesignable和@IBInspectable首妖。我們通過@IBDesignable告訴Interface Builder這個類可以實時渲染到界面中偎漫,無論我們drawRect里面多么復(fù)雜,自定義有多復(fù)雜有缆,Xib / Storyboard都可以把它編譯出來象踊,并且渲染展示出來。但是這個類必須是UIView或者NSView的子類棚壁。通過@IBInspectable可以定義動態(tài)屬性杯矩,即可在Attributes inspector面板中可視化修改屬性值。
@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
這兩個關(guān)鍵字不是今天的重點袖外,看個Demo就會使用了史隆。
Demo地址
如果想看Session的話,可以看這兩個WWDC 2014的鏈接
whats_new_in_xcode_6
whats_new_in_interface_builder
蘋果官方文檔
今天來分享一下我使用這兩個關(guān)鍵字的時候遇到的一些問題和解決過程曼验。
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報的錯誤,并不是運行時的錯誤纯陨。
當(dāng)我們看到Debug的時候坛芽,肯定第一想到的就是點Debug。但是很不幸的是翼抠,在這種情況下咙轩,點擊Debug,每次都會告訴你“Finishing debugging instance of XXXX for interface Builder”阴颖,即使你在你自定義的View里面打了斷點活喊,也無濟于事。
回到問題上來量愧,我們來仔細看看崩潰信息钾菊。信息上說Could not load NIB in bundle,并且還給了我們一個類似地址一樣的東西'NSBundle </Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Overlays> (loaded)'偎肃,我們可以定位到時Xib在從bundle中讀取出來出錯了煞烫。
通過在網(wǎng)上查找資料,問題其實是這樣的累颂。
When loading the nib, we're relying on the fact that passing bundle: nil defaults to your app's mainBundle at run time.
每次我們?nèi)ainBundle的時候滞详,都是用的默認的方法
let nib = UINib(nibName: String(StripyView), bundle: nil)
這里在Xib / Storyboard 編譯的時候,我們需要告訴iOS系統(tǒng)紊馏,我們要指定哪一個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的代碼之后還是不顯示悦荒,那可能是你代碼加的地方不對。
如果是代碼手動創(chuàng)建控件的話团甲,會調(diào)用initWithFrame方法
- (instancetype)initWithFrame:(CGRect)frame
如果是通過Xib / Storyboard 拖拽顯示控件的話,會調(diào)用initWithCoder方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder
需要在對應(yīng)的這兩個方法里面去加上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的運行她按。
但是這個問題會直接導(dǎo)致整個app閃退,直接Crashed掉炕柔!沒辦法酌泰,我們只能打斷點debug一下。
如果你在Designables 那里把Debug打開匕累,然后斷點打到initWithCoder 和 initWithFrame那里陵刹,會發(fā)現(xiàn)程序總是運行到這一行
self = [super initWithCoder:aDecoder];
或者這一行
self = [super initWithFrame:frame];
就崩潰了。其實從下面的棧信息也可以很快看出發(fā)生了什么:
可以很明顯的看到欢嘿,是initWithCoder這個方法陷入了死循環(huán)衰琐。由于這個死循環(huán)導(dǎo)致了程序Crashed了。
可是這里為什么會死循環(huán)呢炼蹦?其實根本原因在于羡宙,我們自定義的類的class寫成自己了。
來看看到底發(fā)生了什么∑現(xiàn)在在Xode 7中狗热,我們默認創(chuàng)建一個View,是不給我們默認生成一個XIB文件瑟枫,ViewController會有下面那個選項斗搞,可以選擇勾上。
在我們創(chuàng)建完這個類的時候慷妙,我們還要再創(chuàng)建一個Xib和這個類進行關(guān)聯(lián)僻焚。
再對比一下我們創(chuàng)建TableviewCell的過程
一般我們會勾選上那個“Also create XIB file”,創(chuàng)建完成之后膝擂,我們就會在Custom Class里面把我們這個cell的類名填上虑啤。
如果我們現(xiàn)在自定義View的時候也是相同做法,創(chuàng)建完Xib文件之后架馋,F(xiàn)ile‘s owner關(guān)聯(lián)好了之后狞山。然后在Custom Class里面填上了我們自定義的類之后,這個時候就錯了叉寂!
為什么我們平時相同的做法萍启,到這里就錯誤了呢?
我們來考慮一下我們自定義View加載的過程。我們這個自定義View肯定是放在了一個ViewController上面勘纯,代碼創(chuàng)建出來或者直接拖拽到Xib / Storyboard 上局服。用代碼或者SB上面拖一個View,這個時候我們需要指定這個類是什么驳遵,這個毋庸置疑淫奔,是絕對沒有問題的。SB上面拖的View的class肯定要選擇我們自定義的這個View堤结。
但是在加載我們這個View的時候唆迁,會走initWithCoder / initWithFrame 方法,在這里方法里面又會去調(diào)用super的這個方法竞穷,現(xiàn)在我們把這個class寫成了自己唐责,依照我們上面調(diào)試的log,可以看到来庭,initWithCoder以后妒蔚,會按照以下的路線去調(diào)用.
[NSBundle loadNibName] —— [UINib instantiateWithOwner:options] ——[UINibDecoder decodeObjectForKey:]——UINibDecoderDecodeObjectForValue——[UIRuntimeConnection initWithCoder]——[UINibDecoder decodeObjectForKey:]——UINibDecoderDecodeObjectForValue——[UIClassSwapper initWithCoder:]——[BottomCommentView initWithCoder:]
從NSBundle加載開始,解析完之后會調(diào)用到ClassSwapper 的initWithCoder月弛,由于我們class寫了自己肴盏,這里就陷入死循環(huán)了。程序崩潰帽衙!這里就跟set方法里面調(diào)用點語法賦值一樣菜皂,無限的遞歸調(diào)用了。
經(jīng)過上面的分析之后厉萝,我們就知道了問題就出在我們在initWithCoder里面又調(diào)用了loadNibName恍飘,loadNibName又會去最終調(diào)UIClassSwapper initWithCoder。難道是我們custom class不對么谴垫?對比一下我們自定義tableViewCell的class就是本身章母,怎么就沒有這個問題呢。
我們來仔細看看tableViewCell我們是怎么加載的翩剪,我們的Xib的class還是自己乳怎,但是registerWithNibName的方法調(diào)用在tableView中,這樣就不會無限遞歸了前弯。
這里當(dāng)然我們也可以仿照這個方法做蚪缀,那我們需要把loadNibName寫到另外一個類中去。class還是寫自己本身恕出,用那個類來加載我們這個View询枚,這樣就可以不崩潰,不會無限遞歸了浙巫。但是問題又來了金蜀,我們無法在Xib/Storyboard上實時預(yù)覽到我們的View了刷后。
這里需要提一下IB_DESIGNABLE的工作原理。當(dāng)我們用了IB_DESIGNABLE關(guān)鍵字以后渊抄,Xib/StoryBoard會在不運行整個程序的情況下惠险,把這個View代碼編譯跑一遍,由于沒有程序上下文抒线,所有的編譯就只在這個view的代碼中進行。
我們在ViewController里面拖拽了一個View渣慕,并且更改它的class為我們自定義的class嘶炭,那么接下來所有view的繪制都會交給我們這個自定義view的class,由這個class來管理逊桦。這里就分兩種情況了眨猎。第一種情況就是我文章一開頭給的Demo的例子,用DrawRect代碼繪制出這個View的樣子强经。這里不會出現(xiàn)任何問題睡陪。第二種情況就是我們還想用一個Xib來顯示View,這種情況就是Xib/StoryBoard里面再次加載Xib的情況了匿情。由于現(xiàn)在我們自定義的class有了接管整個view的繪制權(quán)利兰迫,那么我們就應(yīng)該在initWithCoder中l(wèi)oadNibName,把整個View在初始化的時候load出來炬称。根據(jù)上面的分析汁果,我們找到崩潰的原因是無限遞歸,這里又必須要調(diào)用initWithCoder玲躯,我們的唯一辦法就是把class改成父類的class据德,即UIView,這時候一切就好了跷车,Xib/Storyboard不報錯棘利,也能及時顯示出view的樣子來了。
總結(jié)一下:
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是立即加載劳坑,調(diào)用這個方法加載的xib對象中的各個元素都已經(jīng)存在毕谴。
總結(jié)
當(dāng)我第一次知道IB_DESIGNABLE / IBInspectable之后,感覺到特別的神奇,連我們自定義化的View也可以及時可見了涝开。不過經(jīng)過一段研究以后就發(fā)現(xiàn)循帐。IB_DESIGNABLE / IBInspectable還是有一些缺陷的。IB_DESIGNABLE暫時只能在UIView的子類中用舀武,常用的UIButton加圓角這些暫時也沒法預(yù)覽拄养。IBInspectable實質(zhì)是在Runtime Attributes設(shè)置了值,這也使得IBInspectable只能使用常用類型银舱。NSDate這種類型沒法設(shè)置成IBInspectable瘪匿。
以上就是我和大家分享的IB_DESIGNABLE / IBInspectable使用過程中遇到的一些“坑”。
更新:
下面這一段要感謝@Andy矢倉 微博上面指點我寻馏,其實系統(tǒng)的子類可以這么做:抽了幾個常用的控件的公共類棋弥,順便用External剝離常用屬性,更復(fù)雜的移步這個庫IBAnimatable
@Andy矢倉還提醒說诚欠,用這個特性最好是iOS8 + Swift顽染,OC或者iOS7都會出現(xiàn)Failed to update而且無解,再次感謝@Andy矢倉大神的指點:涿唷7勰!下圖是他對系統(tǒng)控件的可視化改造左腔!