前言艱︰對于接觸業務開發的童鞋拜檬,自定義View的開發是進行最頻繁的工作了姬囤樸。但發現一些童鞋還是沒有以一個好的規範甚至以一種錯誤的方式來搭建UI控件梁。由此農肅,本文將以以下目錄來進行講敘褪森娘,詳細描述關于自定義View的一些書寫注意事項勃俏懦。
關于自定義View的初始化方法
關于addSubview
關于layoutSubviews
關于frame與bounds
一涂霧、關于自定義View的初始化方法
通常我們會創建私有方法createUI方法來創建當前自定義View所需要的子View界娠瀉。那上述所說的createUI應該放在自定義View的哪個方法中呢?
1絛狄、init?
2哺、initWithFrame?
3絡、還是為了考慮外部創建自定義View的方式不同憑董,在init與initWithFrame方法中均調用createUI方法?
我們來一一驗證禽暢,首先在CustomView的init方法中調用createUI方法蒼。
- (instancetype)init {if (self = [super init]) { [self createUI]; }return self; } - (void)createUI { [self addSubview:self.testView]; } - (UIView *)testView {if (!_testView) { _testView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; _testView.backgroundColor = [UIColor redColor]; }return _testView; }復制代碼
外部以init形式創建CustomView
CustomView *customView = [[CustomView alloc] init]; customView.frame = CGRectMake(100, 100, 200, 200); customView.backgroundColor = [UIColor lightGrayColor]; [self.view addSubview:customView];復制代碼
可驗證牆繭,CustomView與其子視圖均可正常顯示伯。但有個問題是娠琳洞,如果外部以initWithFrame形式創建孫,無法調用createUI方法拈,因此子視圖無法顯示箍縣攬。
第二種初始化形式嘎柿,單獨在initWithFrame方法中調用createUI方法
- (instancetype)initWithFrame:(CGRect)frame {if (self = [super initWithFrame:frame]) { [self createUI]; }return self; }復制代碼
可驗證結果是爸紡潑,無論外部以init或者initWithFrame方法初始化CustomView和剎,均可以正常顯示CustomView與其子視圖常革景。
最後我們做個實驗耪堵揮,在init與initWithFrame方法中均調用createUI方法刊構撕。調試createUI方法調用次數嵌鄉遂。
- (instancetype)initWithFrame:(CGRect)frame {if (self = [super initWithFrame:frame]) { [self createUI]; }return self; } - (instancetype)init {if (self = [super init]) { [self createUI]; }return self; } - (void)createUI { NSLog(@"SubViews Add"); [self addSubview:self.testView]; } - (UIView *)testView {if (!_testView) { _testView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; _testView.backgroundColor = [UIColor redColor]; }return _testView; } @end復制代碼
外部創建CustomView仍使用init形式通過打印結果或斷點可驗證createUI方法被執行了兩次!
2019-06-26 17:17:51.961744+0800 TestAddSubview[72346:1989647] SubViews Add 2019-06-26 17:17:51.961917+0800 TestAddSubview[72346:1989647] SubViews Add復制代碼
其實弊城戒,上述三種假設均和一個問題相關筒藩岸,即自定義View的init方法是否會默認調用initWithFrame方法實念。
答案是肯定的逗賒梧,通過上述的代碼調試流程挺鈴,我們可以得到如下結論鶴訟,關于代碼的調用過程(以外部初始化init為例)堪皇︰
1肥丟、動態查找到CustomView的init方法
2陸饒、調用[super init]方法
3襪斑慧、super init方法內部執行的的是[super initWithFrame:CGRectZero]
4壇搐、若super發現CustomView實現了initWithFrame方法
5勒、轉而執行self(CustomView)的initWithFrame方法
6嫁、最後在執行init的其余部分
這里也可以驗證一個結論泄愧︰OC中的super實際上是讓某個類去調用父類的方法犀漆盾,而不是父類去調用某個方法相勺勸,方法動態調用過程順序是由下而上的(這也是為什麼只在init方法中進行createUI不會執行多次的原因脾惕沛,因為父類的initWithFrame沒做createUI操作)單臨。
結論錢︰ createUI方法最好在initWithFrame中調用薪,外部使用init或initWithFrame均可以正常執行createUI方法艙但刪。不要在自定義View中同時重寫init與initWithFrame並執行相同視圖布局代碼咎。會導致布局代碼(createUI)執行多次瘁騷。
二典巳、關于addSubview
我們接著問題一自定義View的初始化方法來說措梯穗,如果同時在init與initWithFrame中同時調用了createUI方法財忱檢,會有什麼影響呢?
顯而易見的是createUI方法執行了多次躬,也就是說重復多次添加了self.testView牢腿艾。那是否會重復添加多個View層呢?
並不會激湍墨,重復多次添加同一個View並不會產生多層級的情況坑釘謝。我們看下addSubview的文檔描述
This method establishes a strong reference to view and sets its next responder to the receiver, which is its new superview. Views can have only one superview. If view already has a superview and that view is not the receiver, this method removes the previous superview before making the receiver its new superview.
大概闡述的意思是澎,View有且僅有一個父視圖唱思理,如果新的父視圖與原父視圖不一樣炭濤,會將View在原視圖中移除澆墾,添加到新視圖上貌。
因此同一父視圖重復添加同一個View並不會產生多層級杭齊稗。 可以簡單通過代碼驗證氯幕慮,我們在createUI中循環添加self.testView酚,最終打印當前視圖的子視圖個數
- (void)createUI {for (NSInteger i = 0; i < 100; i++) { [self addSubview:self.testView]; } NSLog(@"subviewsCount = 【%ld】",self.subviews.count);for (UIView *view in self.subviews) { NSLog(@"subView 【%@】",view); } }復制代碼
運行可見吃盯,視圖的子視圖個數始終為1
2019-06-28 16:02:50.420144+0800 TestAddSubview[78991:832644] subviewsCount = 【1】 2019-06-28 16:02:50.422151+0800 TestAddSubview[78991:832644] subView 【】復制代碼
根據打印結果可驗證態欺舒,CustomView始終只存在一個子視圖(testView)泊賂。
新舊父視圖一致阮哎,我們可以假設隻果做了如下處理樞︰
1謄須、在舊父視圖中移除子視圖鋪,再重新將子視圖添加到父視圖上
2渙綽、判斷新舊父視圖是否一致棋掛凹,若一致糕你,不做任何操作入孔嫉。
因為無法看到addSubview的源碼容敘,猜測可能會有這兩種情況攫閡,個人更偏向第二種處理蓋篙枷。(可重寫子視圖layoutSubviews方法兜冷擺,因為addSubviews會調用layoutSubviews方法鱗貧擠,我們可以調試layoutSubviews的調用次數芹,測試後可驗證addSubviews做了上述二的處理)
結論顯︰若父視圖重復添加同一子視圖逃氰,並不會產生多層級情況女蔣。因為此例中testView是以懶加載的形式創建勉,所以self每次添加的均為同一個View遲污,但如果在createUI中以UIView *testView = [UIView alloc] initWithFrame的形式創建劫容腺,那就會創建出多層級的View頸鞏。
總結室衫︰自定義View的子視圖最好以懶加載形式創建笆奴蘿,可避免因其他書寫不當導致的異常
三兜頹、關于layoutSubviews
關于這一點膏口肝,主要想聊一聊layoutSubviews的調用時機
1謄、setNeedsLayout\ layoutIfNeeded
2竿井、addSubview
3岸開秒、View的大小發生變化豆遼,未變不調用
4凰、UIScrollView滑動
5世、旋轉Screen會觸發父UIView上的layoutSubviews事件
因此對于layoutSubviews的使用我們需要注意以下幾點蹦岸︰
1扳、自定義視圖的init方法並不會調用layoutSubviews
2筐、隻果聲明不要直接調用layoutSubviews方法農,如果需要更新管坷,應該調用setNeedsLayout方法夏吃裙,視圖會在下一次繪制後更新美。如果需要立即更新視圖弗,需要執行layoutIfNeeded方法
3草、因為layoutSubviews調用比較頻繁刊存吞,因此若無特殊需求(文檔所述為執行精確的子視圖布局時可使用)拓,不用重寫layoutSubviews方法撇。
四怕寬鞋、關于frame與bounds
眾所周知琴,在iOS UI控件中有兩個關于位置大小的非常重要的屬性豐吉愛,frame與bounds
UI控件的frame意為相對于該控件父視圖的位置跡輸必,bounds意為相對于控件本身的位置鶴。frame價卡兔、bounds均為結構體CGRect思杠伶,由CGPoint與CGSize組成較,我們可以通過frame.origin/bounds.origin 與frame.size/bounds.size來進行返回控件左上角位置與大小壬殼。
通常給View添加動畫未渭廊,可以直接操作Frame或者得到Layer設置隱式動畫勃浦態。
那如果我們直接操作View的bounds會有什麼情況出現呢?
有如下例子艘槳邢,有三個View熄請,分別為RedView膿、BlueView啃、GreenView蔽肺,RedView添加在當前視圖控制器上盜,BlueView為RedView的子視圖艘,GreenView為BlueView的子視圖僻稅,坐標分別為(10讕虛,10揉翹,200吞,200)儡攆謙、(10嗡氮,10濘,150談粟,150)荷、(10敞,10賢雇,100捻,100)清培匙,其坐標位置如下圖所示草跡剩︰
若修改BlueView的bounds為(0熾膳,10脊耗紋,150達,150)蓮券多,那麼會有什麼情況出現呢?
可能我們通常移動View不會通過bounds而是frame蔽吃,並且也知道bounds是相對于自身的坐標澱,修改其origin不會對其本身產生什麼影響兢,但這就大錯特錯了秤,我們來看此情況的結果炯遍,三個View的展示情況變成了下圖所示桔結嫉︰
BlueView位置並沒有什麼變化禽烷,GreenView卻因為BlueView的修改反,其位置上移了10坐標點!
我們來看下原因管,因為調整里BlueView的bounds侍陵持,導致BlueView相對于自己的坐標上移了10坐標點鄉涂街,GreenView相對于其父視圖的位置也同樣上移了10坐標點苔滅筒。對于GreenView弟逛,他的父視圖BlueView的左上角已經不是(0敬團蝗,0)史屜,而是(0藩擱牽,10)憤殊暮,因此會有上圖的結果唾。
總結魄︰在CustomView中盡量使用frame來做某些操作洞廟,不出于特殊需求儒奇,不要修改bounds的origin屬性隴鏡,會造成難以預期的Bug婪畔百。(不會影響當前視圖費尾,但是會間接影響其子視圖)
好了箍,暫時寫到這里齡火,無規矩不成方圓搽轟,對于開發者一個好的代碼規範往往可以事半功倍貌,共勉!
簡書地址寂︰