你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 關於IB_DESIGNABLE

關於IB_DESIGNABLE

編輯:IOS開發基礎

1194012-0c431c95e1f76510.jpg

投稿文章,作者:一縷殇流化隱半邊冰霜(@halfrost)

前言

IB_DESIGNABLE / IBInspectable 這兩個關鍵字是在WWDC 2014年"What's New in Interface Builder"這個Session裡面,用Swift講過一個例子。也是隨著Xcode 6 新加入的關鍵字。

這兩個關鍵字是用在我們自定義View上的,目前暫時只能用在UIView的子類中,所以系統自帶的原生的那些控件使用這個關鍵字都沒有效果。

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裡面多麼復雜,自定義有多復雜,Xib / Storyboard都可以把它編譯出來,並且渲染展示出來。但是這個類必須是UIView或者NSView的子類。通過@IBInspectable可以定義動態屬性,即可在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

1194012-19b37e597b722379.png

這兩個關鍵字不是今天的重點,看個Demo就會使用了。

Demo地址:https://github.com/halfrost/CircleSlider

如果想看Session的話,可以看這兩個WWDC 2014的鏈接

whats_new_in_xcode_6

whats_new_in_interface_builder

蘋果官方文檔

今天來分享一下我使用這兩個關鍵字的時候遇到的一些問題和解決過程。

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  (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報的錯誤,並不是運行時的錯誤。

1194012-548c7e2d005a5da0.png

當我們看到Debug的時候,肯定第一想到的就是點Debug。但是很不幸的是,在這種情況下,點擊Debug,每次都會告訴你“Finishing debugging instance of XXXX for interface Builder”,即使你在你自定義的View裡面打了斷點,也無濟於事。

回到問題上來,我們來仔細看看崩潰信息。信息上說Could not load NIB in bundle,並且還給了我們一個類似地址一樣的東西'NSBundle (loaded)',我們可以定位到時Xib在從bundle中讀取出來出錯了。

通過在網上查找資料,問題其實是這樣的。

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];

就崩潰了。其實從下面的棧信息也可以很快看出發生了什麼:

1194012-ee0d98669a4889f8.jpg

1194012-ca3fcc8e1ef9a9e6.jpg

可以很明顯的看到,是initWithCoder這個方法陷入了死循環。由於這個死循環導致了程序Crashed了。

可是這裡為什麼會死循環呢?其實根本原因在於,我們自定義的類的class寫成自己了。

來看看到底發生了什麼。現在在Xode 7中,我們默認創建一個View,是不給我們默認生成一個XIB文件,ViewController會有下面那個選項,可以選擇勾上。

1194012-747a4d232003df04.jpg

在我們創建完這個類的時候,我們還要再創建一個Xib和這個類進行關聯。

再對比一下我們創建TableviewCell的過程

1194012-736c5bf9967f48af.jpg

一般我們會勾選上那個“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使用過程中遇到的一些“坑”。

更新:

下面這一段要感謝@Andy矢倉 微博上面指點我,其實系統的子類可以這麼做:抽了幾個常用的控件的公共類,順便用External剝離常用屬性,更復雜的移步這個庫IBAnimatable。

@Andy矢倉還提醒說,用這個特性最好是iOS8 + Swift,OC或者iOS7都會出現Failed to update而且無解,再次感謝@Andy矢倉大神的指點!!!下圖是他對系統控件的可視化改造!

1470191533580447.png

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved