如何寫好一個自定義View

王天池發表於2018-10-01

前言

對於iOS開發來說,寫一個自定義view,或者恰當地使用tableview基本上可以算的上是“行活”。但是看過一些同學寫的自定義控制元件後,有時感覺似乎寫的不夠好,雖然可以正常工作,但是在可擴充性、易用性、以及穩定性上都有所欠缺。所以我打算寫一個系列,就叫做如何寫好xxx,就總結下我認為的好的寫法應該是什麼樣的,這篇便是這系列的第一篇。

當然受視野和水平所限,文章中提到的一些東西並不一定是最優解,非常歡迎大家提出不同的意見,討論後共同成長!

目標

  • 使用方式多樣
    • 純程式碼中使用
    • Xib/storyboard中使用
  • 使用的易用性
    • 儘量簡單的介面設計
    • 儘量少的暴露實現
    • 對異常情況的處理

實現

初始化方法

這裡我們大可借鑑一下UIKit中系統的UI元件是如何設計自己的初始化方法的。

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,提供便利的初始化方法,根據需要為某些屬性提供預設值,方法內部實現最終還是會呼叫Designated initializer;

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

實現Designated initializer

為了既能滿足純程式碼的方式,又能使用Xib的方式,我們需要實現CustomView的兩個Designated initializer

而且在swift中,initWithCoder已經被標記為required,所以必須要實現啦

在實現這兩個方式時,主要做的就是新增子view,以及提供預設值

提供Convenience Initializer

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

使用Convenience Initializer的好處也是顯而易見的,能讓類的使用者很清楚的知道我應該如何正確的初始化這個類。而且會對必需的屬性提供預設值,既能極大的避免了呼叫者只呼叫init,導致該例項並不能正常工作,又能在很多屬性時,提供一個簡單的初始化方法。

內部子view佈局的實現

frame or autoLayout?

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

如果使用autoLayout,就不存在上面的問題,唯一一個需要考慮的問題便是效能了。通過WWDC也可以知道,雖然蘋果對於autolayout一再優化,仍然在多檢視情境下,效能遠不如frame

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

構建檢視

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

  • -(void)drawRect:(CGRect)rect
    • 使用場景:需要使用Core Graphics或者UIKit繪製頁面時,如果是使用已有UI控制元件addsubview組合自己的view,則不需要重寫這個方法
    • 被呼叫時機:view首次顯示的時候,或者某個事件導致了view需要更新,不要直接手動呼叫。如果需要重繪,呼叫 setNeedsDisplay 或者 setNeedsDisplayInRect:
    • 引數說明:view需要更新的範圍,如果是連續的繪製,那麼rect可能只是view的一部分
  • -(void)layoutSubviews
    • 使用場景:只有當autoresizing和約束不能滿足你的需求時,才重寫layoutSubviews來提供更精確的佈局
    • 被呼叫時機:不要直接手動呼叫,呼叫setNeedsLayout來更新約束,如果需要立即更新約束,那麼呼叫 layoutIfNeeded
  • -(void)updateConstraints;
    • 使用場景:為了優化約束的變化,需要提早改變約束,或者產生大量冗餘修改時。
    • 被呼叫時機:不要直接手動呼叫,在view需要修改約束的時候,呼叫setNeedsUpdateConstraints
    • 注意事項:
      • 在實現的最後,呼叫[Super updateConstraints]
      • 不要在方法實現中呼叫setNeedsUpdateConstraints,會產生迴圈

介面的設計

介面設計儘量遵循Effective Objective-C 2.0中的建議,比如必須暴露的屬性儘量為readonly,內部實現的私有方法不必暴露出去。在設計介面的時候,時刻要想著,這個方法,這個屬性真的有必要讓別人知道嗎?這個方法真正的目的是什麼?總之,儘量遵循Keep It Simple, Stupid就對了。

執行緒管理

所有UI的操作都應該在主執行緒進行,這需要我們在涉及到UI變動的方法中,確保是主執行緒,而不依賴使用者

一個簡單的例子

TCZoomingImageView是基於UIScrollView和UIImageView做一個可縮放的View,實現非常的簡單,僅作為一個簡單的例子,拋磚引玉。

GitHub地址:TCZoomingImageView

參考資料

UIView Document From Apple

View Programming Guide for iOS

iOS 建立物件的姿勢

Object Initialization

從 Auto Layout 的佈局演算法談效能

WWDC:High Performance Auto Layout

WWDC:Mysteries of Auto Layout, Part 1

[WWDC:Mysteries of Auto Layout, Part 2

相關文章