前言
對于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
參考資料
View Programming Guide for iOS
WWDC:High Performance Auto Layout
WWDC:Mysteries of Auto Layout, Part 1
[WWDC:Mysteries of Auto Layout, Part 2