如何寫好一個自定義View

前言

對于iOS開發(fā)來說,寫一個自定義view,或者恰當(dāng)?shù)厥褂胻ableview基本上可以算的上是“行活”。但是看過一些同學(xué)寫的自定義控件后,有時感覺似乎寫的不夠好,雖然可以正常工作,但是在可拓展性、易用性、以及穩(wěn)定性上都有所欠缺。所以我打算寫一個系列,就叫做如何寫好xxx,就總結(jié)下我認(rèn)為的好的寫法應(yīng)該是什么樣的,這篇便是這系列的第一篇。

當(dāng)然受視野和水平所限,文章中提到的一些東西并不一定是最優(yōu)解,非常歡迎大家提出不同的意見,討論后共同成長!

目標(biāo)

  • 使用方式多樣
    • 純代碼中使用
    • Xib/storyboard中使用
  • 使用的易用性
    • 盡量簡單的接口設(shè)計(jì)
    • 盡量少的暴露實(shí)現(xiàn)
    • 對異常情況的處理

實(shí)現(xiàn)

初始化方法

這里我們大可借鑒一下UIKit中系統(tǒng)的UI組件是如何設(shè)計(jì)自己的初始化方法的。

UIKit中初始化方法大概分為兩類,

  • 繼承自父類的Designated initializer

    • initWithFrame
    • initWithCoder(不是所有的UI類都繼承UIView,例如繼承NSObject的UIBarItem,這些就沒有initWithFrame方法)
  • Convenience Initializer,例如UITabBarItem中的

    - (instancetype)initWithTitle:(nullable NSString *)title image:(nullable UIImage *)image tag:(NSInteger)tag;
    - (instancetype)initWithTitle:(nullable NSString *)title image:(nullable UIImage *)image selectedImage:(nullable UIImage *)selectedImage NS_AVAILABLE_IOS(7_0);
    - (instancetype)initWithTabBarSystemItem:(UITabBarSystemItem)systemItem tag:(NSInteger)tag;
    

    等方法

首先,我們要搞清楚什么是Designated initializer和Convenience Initializer。

  • Designated initializer,初始化類必須有的屬性
  • Convenience Initializer,提供便利的初始化方法,根據(jù)需要為某些屬性提供默認(rèn)值,方法內(nèi)部實(shí)現(xiàn)最終還是會調(diào)用Designated initializer;

其次,為什么UIView的子類都會有兩個Designated initializer呢?這里就是我們之前提到的,View的兩種使用方法,Xib/storyboard,和純代碼。

實(shí)現(xiàn)Designated initializer

為了既能滿足純代碼的方式,又能使用Xib的方式,我們需要實(shí)現(xiàn)CustomView的兩個Designated initializer

而且在swift中,initWithCoder已經(jīng)被標(biāo)記為required,所以必須要實(shí)現(xiàn)啦

在實(shí)現(xiàn)這兩個方式時,主要做的就是添加子view,以及提供默認(rèn)值

提供Convenience Initializer

例如UIImageView,他就提供了initWithImage 這個Convenience Initializer。

使用Convenience Initializer的好處也是顯而易見的,能讓類的使用者很清楚的知道我應(yīng)該如何正確的初始化這個類。而且會對必需的屬性提供默認(rèn)值,既能極大的避免了調(diào)用者只調(diào)用init,導(dǎo)致該實(shí)例并不能正常工作,又能在很多屬性時,提供一個簡單的初始化方法。

內(nèi)部子view布局的實(shí)現(xiàn)

frame or autoLayout?

如果使用frame,我們需要保證custom view自己的size發(fā)生變化的時候,subviews能夠自動變化,而不是保持原有的frame。(autoresizingMask,autoresizesSubviews)

如果使用autoLayout,就不存在上面的問題,唯一一個需要考慮的問題便是性能了。通過WWDC也可以知道,雖然蘋果對于autolayout一再優(yōu)化,仍然在多視圖情境下,性能遠(yuǎn)不如frame

我的觀點(diǎn) :如果頁面層級不復(fù)雜,性能差別也不大,我還是傾向使用AutoLayout,畢竟算frame也是比較麻煩,而且代碼可讀性也要比AutoLayout差很多

構(gòu)建視圖

這時需要解釋幾個很重要的方法,以及什么時候需要使用

  • -(void)drawRect:(CGRect)rect
    • 使用場景:需要使用Core Graphics或者UIKit繪制頁面時,如果是使用已有UI控件addsubview組合自己的view,則不需要重寫這個方法
    • 被調(diào)用時機(jī):view首次顯示的時候,或者某個事件導(dǎo)致了view需要更新,不要直接手動調(diào)用。如果需要重繪,調(diào)用 setNeedsDisplay 或者 setNeedsDisplayInRect:
    • 參數(shù)說明:view需要更新的范圍,如果是連續(xù)的繪制,那么rect可能只是view的一部分
  • -(void)layoutSubviews
    • 使用場景:只有當(dāng)autoresizing和約束不能滿足你的需求時,才重寫layoutSubviews來提供更精確的布局
    • 被調(diào)用時機(jī):不要直接手動調(diào)用,調(diào)用setNeedsLayout來更新約束,如果需要立即更新約束,那么調(diào)用 layoutIfNeeded
  • -(void)updateConstraints;
    • 使用場景:為了優(yōu)化約束的變化,需要提早改變約束,或者產(chǎn)生大量冗余修改時。
    • 被調(diào)用時機(jī):不要直接手動調(diào)用,在view需要修改約束的時候,調(diào)用setNeedsUpdateConstraints
    • 注意事項(xiàng):
      • 在實(shí)現(xiàn)的最后,調(diào)用[Super updateConstraints]
      • 不要在方法實(shí)現(xiàn)中調(diào)用setNeedsUpdateConstraints,會產(chǎn)生循環(huán)

接口的設(shè)計(jì)

接口設(shè)計(jì)盡量遵循Effective Objective-C 2.0中的建議,比如必須暴露的屬性盡量為readonly,內(nèi)部實(shí)現(xiàn)的私有方法不必暴露出去。在設(shè)計(jì)接口的時候,時刻要想著,這個方法,這個屬性真的有必要讓別人知道嗎?這個方法真正的目的是什么?總之,盡量遵循Keep It Simple, Stupid就對了。

線程管理

所有UI的操作都應(yīng)該在主線程進(jìn)行,這需要我們在涉及到UI變動的方法中,確保是主線程,而不依賴使用者

一個簡單的例子

TCZoomingImageView是基于UIScrollView和UIImageView做一個可縮放的View,實(shí)現(xiàn)非常的簡單,僅作為一個簡單的例子,拋磚引玉。

GitHub地址:TCZoomingImageView

參考資料

UIView Document From Apple

View Programming Guide for iOS

iOS 創(chuàng)建對象的姿勢

Object Initialization

從 Auto Layout 的布局算法談性能

WWDC:High Performance Auto Layout

WWDC:Mysteries of Auto Layout, Part 1

[WWDC:Mysteries of Auto Layout, Part 2

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容