UIView
的首要問題就是既能從代碼中初始化,也能從 xib
中初始化,兩者有何不同?
UIView 是支持 NSCoding
協議的,當在 xib 或 storyboard 裏存在一個 UIView 的時候,其實是將 UIView 序列化到文件裏(xib 和 storyboard 都是以 XML 格式來保存的),加載的時候反序列化出來,所以:
- 當從代碼實例化 UIView 的時候,
initWithFrame
會執行;- 當從文件加載 UIView 的時候,
initWithCoder
會執行。
雖然 initWithFrame 是 UIView 的Designated Initializer
,理論上來講你繼承自 UIView 的任何子類,該方法最終都會被調用,但是有一些類在初始化的時候沒有遵守這個約定,如 UIImageView
的 initWithImage
和 UITableViewCell
的 initWithStyle:reuseIdentifier
:
的構造器等,所以我們在寫自定義控件的時候,最好只假設父視圖的 Designated Initializer 被調用。
如果控件在初始化或者在使用之前必須有一些參數要設置,那我們可以寫自己的 Designated Initializer 構造器,如:
<code class="hljs erlang has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-pp" style="box-sizing: border-box;">- <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(instancetype)</span>initWithName:<span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(<span class="hljs-variable" style="box-sizing: border-box;">NSString</span> *)</span>name;</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>
在實現中一定要調用父類的 Designated Initializer,而且如果你有多個自定義的 Designated Initializer,最終都應該指向一個全能的初始化構造器:
<code class="hljs objectivec has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">- (instancetype)initWithName:(<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">NSString</span> *)name { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> = [<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> initWithName:name frame:CGRectZero]; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>; } - (instancetype)initWithName:(<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">NSString</span> *)name frame:(<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">CGRect</span>)frame { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> = [<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span> initWithFrame:frame]; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>) { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span><span class="hljs-variable" style="color: rgb(102, 0, 102); box-sizing: border-box;">.name</span> = name; } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>; }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li></ul>
並且你要考慮到,因爲你的控件是繼承自 UIView 或 UIControl 的,那麼用戶完全可以不使用你提供的構造器,而直接調用基類的構造器,所以最好重寫父類的 Designated Initializer,使它調用你提供的 Designated Initializer ,比如父類是個 UIView:
<code class="hljs objectivec has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">- (instancetype)initWithFrame:(<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">CGRect</span>)frame { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> = [<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> initWithName:<span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">nil</span> frame:frame]; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>; }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li></ul>
這樣當用戶從代碼裏初始化你的控件的時候,就總是逃脫不了你需要執行的初始化代碼了,哪怕用戶直接調用 init
方法,最終還是會回到父類的 Designated Initializer 上。
從xib或者storyboard中加載
當控件從 xib 或 storyboard 中加載的時候,情況就變得複雜了,首先我們知道有 initWithCoder 方法,該方法會在對象被反序列化的時候調用,比如從文件加載一個 UIView 的時候:
<code class="hljs haskell has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">UIView</span> *view = [[<span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">UIView</span> alloc] init]; <span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">NSData</span> *<span class="hljs-typedef" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">data</span> = [<span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">NSKeyedArchiver</span> archivedDataWithRootObject:view];</span> [[<span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">NSUserDefaults</span> standardUserDefaults] setObject:<span class="hljs-typedef" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">data</span> forKey:@"<span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">KeyView</span>"];</span> [[<span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">NSUserDefaults</span> standardUserDefaults] synchronize]; <span class="hljs-typedef" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">data</span> = [[<span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">NSUserDefaults</span> standardUserDefaults] objectForKey:@"<span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">KeyView</span>"];</span> <span class="hljs-title" style="box-sizing: border-box;">view</span> = [<span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">NSKeyedUnarchiver</span> unarchiveObjectWithData:<span class="hljs-typedef" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">data</span>];</span> <span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">NSLog</span>(@<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"%@"</span>, view);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li></ul>
執行 unarchiveObjectWithData
的時候, initWithCoder
會被調用,那麼你有可能會在這個方法裏做一些初始化工作,比如恢復到保存之前的狀態,當然前提是需要在 encodeWithCoder
中預先保存下來。
不過我們很少會自己直接把一個 View 保存到文件中,一般是在 xib 或 storyboard 中寫一個 View,然後讓系統來完成反序列化的工作,此時在initWithCoder
調用之後,awakeFromNib
方法也會被執行,既然在 awakeFromNib
方法裏也能做初始化操作,那我們如何抉擇?
一般來說要儘量在 initWithCoder
中做初始化操作,畢竟這是最合理的地方,只要你的控件支持序列化,那麼它就能在任何被反序列化的時候執行初始化操作,這裏適合做全局數據、狀態的初始化工作,也適合手動添加子視圖。
awakeFromNib
相較於 initWithCoder
的優勢是:當 awakeFromNib
執行的時候,各種 IBOutlet
也都連接好了;而 initWithCoder
調用的時候,雖然子視圖已經被添加到視圖層級中,但是還沒有引用。如果你是基於
xib 或 storyboard 創建的控件,那麼你可能需要對 IBOutlet 連接的子控件進行初始化工作,這種情況下,你只能在 awakeFromNib
裏進行處理。同時 xib 或 storyboard 對靈活性是有打折的,因爲它們創建的代碼無法被繼承,所以當你選擇用
xib 或 storyboard 來實現一個控件的時候,你已經不需要對靈活性有很高的要求了,唯一要做的是要保證用戶一定是通過 xib 創建的此控件,否則可能是一個空的視圖,可以在 initWithFrame
裏放置一個 斷言
或者異常來通知控件的用戶。
最後還要注意視圖層級的問題,比如你要給 View 放置一個背景,你可能會在 initWithCoder
或 awakeFromNib
中這樣寫:
<code class="hljs ruby has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">[<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> <span class="hljs-symbol" style="color: rgb(0, 102, 102); box-sizing: border-box;">addSubview:</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>.backgroundView]; <span class="hljs-regexp" style="color: rgb(0, 136, 0); box-sizing: border-box;">//</span> 通過懶加載一個背景 <span class="hljs-constant" style="box-sizing: border-box;">View</span>,然後添加到視圖層級上</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>
你的本意是在控件的最下面放置一個背景,卻有可能將這個背景覆蓋到控件的最上方,原因是用戶可能會在 xib 裏寫入這個控件,然後往它上面添加一些子視圖,這樣一來,用戶添加的這些子視圖會在你添加背景之前先進入視圖層級,你的背景被添加後就擋住了用戶的子視圖。如果你想支持用戶的這種操作,可以把 addSubview
替換成 insertSubview:atIndex:
。
如果你要同時支持 initWithFrame
和 initWithCoder
,那麼你可以提供一個 commonInit
方法來做統一的初始化:
<code class="hljs objectivec has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">- (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">id</span>)initWithCoder:(NSCoder *)aDecoder { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> = [<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span> initWithCoder:aDecoder]; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>) { [<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> commonInit]; } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>; } - (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">id</span>)initWithFrame:(<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">CGRect</span>)frame { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> = [<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span> initWithFrame:frame]; <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>) { [<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> commonInit]; } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>; } - (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span>)commonInit { <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// do something ...</span> }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li></ul>
awakeFromNib
方法裏就不要再去調用 commonInit
了。