关于IB_DESIGNABLE / IBInspectable的那些事
?
?
前言
?
IB_DESIGNABLE / IBInspectable 這兩個(gè)關(guān)鍵字是在WWDC 2014年”What’s New in Interface Builder”這個(gè)Session里面,用Swift講過(guò)一個(gè)例子。也是隨著Xcode 6 新加入的關(guān)鍵字。
?
這兩個(gè)關(guān)鍵字是用在我們自定義View上的,目前暫時(shí)只能用在UIView的子類(lèi)中所以系統(tǒng)自帶的原生的那些控件使用這個(gè)關(guān)鍵字都沒(méi)有效果。
?
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.
?
其大意就是說(shuō),“所見(jiàn)即所得”的思想,我們可以將自定義的代碼實(shí)時(shí)渲染到Interface Builder中。而它們之間的橋梁就是通過(guò)兩個(gè)指令來(lái)完成,即@IBDesignable和@IBInspectable。我們通過(guò)@IBDesignable告訴Interface Builder這個(gè)類(lèi)可以實(shí)時(shí)渲染到界面中,無(wú)論我們drawRect里面多么復(fù)雜,自定義有多復(fù)雜,Xib / Storyboard都可以把它編譯出來(lái),并且渲染展示出來(lái)。但是這個(gè)類(lèi)必須是UIView或者NSView的子類(lèi)。通過(guò)@IBInspectable可以定義動(dòng)態(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
?
?
這兩個(gè)關(guān)鍵字不是今天的重點(diǎn),看個(gè)Demo就會(huì)使用了。
Demo地址:https://github.com/halfrost/CircleSlider
?
如果想看Session的話,可以看這兩個(gè)WWDC 2014的鏈接
whats_new_in_xcode_6:http://t.cn/RtyJKbg
whats_new_in_interface_builder:http://t.cn/RtyJCrB
蘋(píng)果官方文檔:http://t.cn/RtyJpCq
?
今天來(lái)分享一下我使用這兩個(gè)關(guān)鍵字的時(shí)候遇到的一些問(wèn)題和解決過(guò)程。
?
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 /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.
?
我們會(huì)看到面板上Designables這里顯示的是一個(gè)Crashed,Xib / Storyboard 居然也會(huì)Crashed!整個(gè)app是跑起來(lái)了,但是報(bào)了2個(gè)錯(cuò),不能忍!這兩個(gè)錯(cuò)其實(shí)是編譯時(shí)候Xib報(bào)的錯(cuò)誤,并不是運(yùn)行時(shí)的錯(cuò)誤。
?
?
當(dāng)我們看到Debug的時(shí)候,肯定第一想到的就是點(diǎn)Debug。但是很不幸的是,在這種情況下,點(diǎn)擊Debug,每次都會(huì)告訴你“Finishing debugging instance of XXXX for interface Builder”,即使你在你自定義的View里面打了斷點(diǎn),也無(wú)濟(jì)于事。
?
回到問(wèn)題上來(lái),我們來(lái)仔細(xì)看看崩潰信息。信息上說(shuō)Could not load NIB in bundle,并且還給了我們一個(gè)類(lèi)似地址一樣的東西’NSBundle /Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Overlays> (loaded)’,我們可以定位到時(shí)Xib在從bundle中讀取出來(lái)出錯(cuò)了。
?
通過(guò)在網(wǎng)上查找資料,問(wèn)題其實(shí)是這樣的。
?
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的時(shí)候,都是用的默認(rèn)的方法
?
let nib = UINib(nibName: String(StripyView), bundle: nil)
?
這里在Xib / Storyboard 編譯的時(shí)候,我們需要告訴iOS系統(tǒng),我們要指定哪一個(gè)bundle類(lèi)去讀取。把上面的代碼改成下面這樣就可以了。
?
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上,但是程序一運(yùn)行就又能顯示出View來(lái),原因也有可能是這個(gè)原因,雖然Xib / Storyboard沒(méi)有報(bào)錯(cuò),因?yàn)閍pp沒(méi)有運(yùn)行起來(lái),Xib / Storyboard并不知道上下文,所以沒(méi)有把我們自定義的View加載出來(lái)。
?
2.代碼或者Xib依舊不顯示自定義控件的樣子
?
如果你按照上面的第一個(gè)問(wèn)題里面加上了bundle的代碼之后還是不顯示,那可能是你代碼加的地方不對(duì)。
?
如果是代碼手動(dòng)創(chuàng)建控件的話,會(huì)調(diào)用initWithFrame方法
?
- (instancetype)initWithFrame:(CGRect)frame
?
如果是通過(guò)Xib / Storyboard 拖拽顯示控件的話,會(huì)調(diào)用initWithCoder方法
?
- (instancetype)initWithCoder:(NSCoder *)aDecoder
?
需要在對(duì)應(yīng)的這兩個(gè)方法里面去加上bundle的方法。如果為了保險(xiǎn)起見(jiàn),那這兩個(gè)init方法里面都加上問(wèn)題一里面的代碼吧。
?
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
?
如果是遇到了這個(gè)問(wèn)題,是比較嚴(yán)重的,這個(gè)問(wèn)題不像問(wèn)題一,問(wèn)題一整個(gè)app是可以運(yùn)行的,錯(cuò)誤來(lái)源于Xib / Storyboard編譯時(shí)候的錯(cuò)誤,但是并不影響這個(gè)app的運(yùn)行。
?
但是這個(gè)問(wèn)題會(huì)直接導(dǎo)致整個(gè)app閃退,直接Crashed掉!沒(méi)辦法,我們只能打斷點(diǎn)debug一下。
?
如果你在Designables 那里把Debug打開(kāi),然后斷點(diǎn)打到initWithCoder 和 initWithFrame那里,會(huì)發(fā)現(xiàn)程序總是運(yùn)行到這一行
?
self = [super initWithCoder:aDecoder];
?
或者這一行
?
self = [super initWithFrame:frame];
?
就崩潰了。其實(shí)從下面的棧信息也可以很快看出發(fā)生了什么:
?
?
?
可以很明顯的看到,是initWithCoder這個(gè)方法陷入了死循環(huán)。由于這個(gè)死循環(huán)導(dǎo)致了程序Crashed了。
?
可是這里為什么會(huì)死循環(huán)呢?其實(shí)根本原因在于,我們自定義的類(lèi)的class寫(xiě)成自己了。
?
來(lái)看看到底發(fā)生了什么。現(xiàn)在在Xode 7中,我們默認(rèn)創(chuàng)建一個(gè)View,是不給我們默認(rèn)生成一個(gè)XIB文件,ViewController會(huì)有下面那個(gè)選項(xiàng),可以選擇勾上。
?
?
在我們創(chuàng)建完這個(gè)類(lèi)的時(shí)候,我們還要再創(chuàng)建一個(gè)Xib和這個(gè)類(lèi)進(jìn)行關(guān)聯(lián)。
?
再對(duì)比一下我們創(chuàng)建TableviewCell的過(guò)程
?
?
一般我們會(huì)勾選上那個(gè)“Also create XIB file”,創(chuàng)建完成之后,我們就會(huì)在Custom Class里面把我們這個(gè)cell的類(lèi)名填上。
?
如果我們現(xiàn)在自定義View的時(shí)候也是相同做法,創(chuàng)建完Xib文件之后,File‘s owner關(guān)聯(lián)好了之后。然后在Custom Class里面填上了我們自定義的類(lèi)之后,這個(gè)時(shí)候就錯(cuò)了!
?
為什么我們平時(shí)相同的做法,到這里就錯(cuò)誤了呢?
?
我們來(lái)考慮一下我們自定義View加載的過(guò)程。我們這個(gè)自定義View肯定是放在了一個(gè)ViewController上面,代碼創(chuàng)建出來(lái)或者直接拖拽到Xib / Storyboard 上。用代碼或者SB上面拖一個(gè)View,這個(gè)時(shí)候我們需要指定這個(gè)類(lèi)是什么,這個(gè)毋庸置疑,是絕對(duì)沒(méi)有問(wèn)題的。SB上面拖的View的class肯定要選擇我們自定義的這個(gè)View。
?
但是在加載我們這個(gè)View的時(shí)候,會(huì)走initWithCoder / initWithFrame 方法,在這里方法里面又會(huì)去調(diào)用super的這個(gè)方法,現(xiàn)在我們把這個(gè)class寫(xiě)成了自己,依照我們上面調(diào)試的log,可以看到,initWithCoder以后,會(huì)按照以下的路線去調(diào)用.
?
[NSBundle loadNibName] —— [UINib instantiateWithOwner:options] ——[UINibDecoder decodeObjectForKey:]——UINibDecoderDecodeObjectForValue——[UIRuntimeConnection initWithCoder]——[UINibDecoder decodeObjectForKey:]——UINibDecoderDecodeObjectForValue——[UIClassSwapper initWithCoder:]——[BottomCommentView initWithCoder:]
?
從NSBundle加載開(kāi)始,解析完之后會(huì)調(diào)用到ClassSwapper 的initWithCoder,由于我們class寫(xiě)了自己,這里就陷入死循環(huán)了。程序崩潰!這里就跟set方法里面調(diào)用點(diǎn)語(yǔ)法賦值一樣,無(wú)限的遞歸調(diào)用了。
?
經(jīng)過(guò)上面的分析之后,我們就知道了問(wèn)題就出在我們?cè)趇nitWithCoder里面又調(diào)用了loadNibName,loadNibName又會(huì)去最終調(diào)UIClassSwapper initWithCoder。難道是我們custom class不對(duì)么?對(duì)比一下我們自定義tableViewCell的class就是本身,怎么就沒(méi)有這個(gè)問(wèn)題呢。
?
我們來(lái)仔細(xì)看看tableViewCell我們是怎么加載的,我們的Xib的class還是自己,但是registerWithNibName的方法調(diào)用在tableView中,這樣就不會(huì)無(wú)限遞歸了。
?
這里當(dāng)然我們也可以仿照這個(gè)方法做,那我們需要把loadNibName寫(xiě)到另外一個(gè)類(lèi)中去。class還是寫(xiě)自己本身,用那個(gè)類(lèi)來(lái)加載我們這個(gè)View,這樣就可以不崩潰,不會(huì)無(wú)限遞歸了。但是問(wèn)題又來(lái)了,我們無(wú)法在Xib/Storyboard上實(shí)時(shí)預(yù)覽到我們的View了。
?
這里需要提一下IB_DESIGNABLE的工作原理。當(dāng)我們用了IB_DESIGNABLE關(guān)鍵字以后,Xib/StoryBoard會(huì)在不運(yùn)行整個(gè)程序的情況下,把這個(gè)View代碼編譯跑一遍,由于沒(méi)有程序上下文,所有的編譯就只在這個(gè)view的代碼中進(jìn)行。
?
我們?cè)赩iewController里面拖拽了一個(gè)View,并且更改它的class為我們自定義的class,那么接下來(lái)所有view的繪制都會(huì)交給我們這個(gè)自定義view的class,由這個(gè)class來(lái)管理。這里就分兩種情況了。第一種情況就是我文章一開(kāi)頭給的Demo的例子,用DrawRect代碼繪制出這個(gè)View的樣子。這里不會(huì)出現(xiàn)任何問(wèn)題。第二種情況就是我們還想用一個(gè)Xib來(lái)顯示View,這種情況就是Xib/StoryBoard里面再次加載Xib的情況了。由于現(xiàn)在我們自定義的class有了接管整個(gè)view的繪制權(quán)利,那么我們就應(yīng)該在initWithCoder中l(wèi)oadNibName,把整個(gè)View在初始化的時(shí)候load出來(lái)。根據(jù)上面的分析,我們找到崩潰的原因是無(wú)限遞歸,這里又必須要調(diào)用initWithCoder,我們的唯一辦法就是把class改成父類(lèi)的class,即UIView,這時(shí)候一切就好了,Xib/Storyboard不報(bào)錯(cuò),也能及時(shí)顯示出view的樣子來(lái)了。
?
總結(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.這里說(shuō)的僅僅是loadNibNamed而不是initWithNibName。順帶提一下他們倆的不同點(diǎn)。initWithNibName要加載的Xib的類(lèi)為我們定義的ViewController。loadNibNamed要加載的Xib的類(lèi)為NSOjbect。他們的加載方式也不同,initWithNibName方法:是延遲加載,這個(gè)View上的控件是 nil 的,只有到需要顯示時(shí),才會(huì)不是 nil。loadNibNamed是立即加載,調(diào)用這個(gè)方法加載的xib對(duì)象中的各個(gè)元素都已經(jīng)存在。
?
總結(jié)
?
當(dāng)我第一次知道IB_DESIGNABLE / IBInspectable之后,感覺(jué)到特別的神奇,連我們自定義化的View也可以及時(shí)可見(jiàn)了。不過(guò)經(jīng)過(guò)一段研究以后就發(fā)現(xiàn)。IB_DESIGNABLE / IBInspectable還是有一些缺陷的。IB_DESIGNABLE暫時(shí)只能在UIView的子類(lèi)中用,常用的UIButton加圓角這些暫時(shí)也沒(méi)法預(yù)覽。IBInspectable實(shí)質(zhì)是在Runtime Attributes設(shè)置了值,這也使得IBInspectable只能使用常用類(lèi)型。NSDate這種類(lèi)型沒(méi)法設(shè)置成IBInspectable。
?
以上就是我和大家分享的IB_DESIGNABLE / IBInspectable使用過(guò)程中遇到的一些“坑”。
?
更新:
?
下面這一段要感謝@Andy矢?jìng)} 微博上面指點(diǎn)我,其實(shí)系統(tǒng)的子類(lèi)可以這么做:抽了幾個(gè)常用的控件的公共類(lèi),順便用External剝離常用屬性,更復(fù)雜的移步這個(gè)庫(kù)IBAnimatable
?
@Andy矢?jìng)}還提醒說(shuō),用這個(gè)特性最好是iOS8 + Swift,OC或者iOS7都會(huì)出現(xiàn)Failed to update而且無(wú)解,再次感謝@Andy矢?jìng)}大神的指點(diǎn)!!!下圖是他對(duì)系統(tǒng)控件的可視化改造!
?
?
總結(jié)
以上是生活随笔為你收集整理的关于IB_DESIGNABLE / IBInspectable的那些事的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 使用Docker Compose部署基于
- 下一篇: 如何改变Myeclipse编辑区背景色