選擇正確的初始化方式

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 了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章