前言:對於接觸業務開發的童鞋,自定義View的開發是進行最頻繁的工做了。但發現一些童鞋仍是沒有以一個好的規範甚至以一種錯誤的方式來搭建UI控件。由此,本文將以如下目錄來進行講敘,詳細描述關於自定義View的一些書寫注意事項。bash
1、關於自定義View的初始化方法markdown
一般咱們會建立私有方法createUI方法來建立當前自定義View所須要的子View。那上述所說的createUI應該放在自定義View的哪一個方法中呢?網絡
一、init?oop
二、initWithFrame?佈局
三、仍是爲了考慮外部建立自定義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形式建立CustomViewthis
CustomView *customView = [[CustomView alloc] init];
customView.frame = CGRectMake(100, 100, 200, 200);
customView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:customView];
複製代碼
可驗證,CustomView與其子視圖都可正常顯示。但有個問題是,若是外部以initWithFrame形式建立,沒法調用createUI方法,所以子視圖沒法顯示。spa
第二種初始化形式,單獨在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爲例):
一、動態查找到CustomView的init方法
二、調用[super init]方法
三、super init方法內部執行的的是[super initWithFrame:CGRectZero]
四、若super發現CustomView實現了initWithFrame方法
五、轉而執行self(CustomView)的initWithFrame方法
六、最後在執行init的其他部分
這裏也能夠驗證一個結論:OC中的super其實是讓某個類去調用父類的方法,而不是父類去調用某個方法,方法動態調用過程順序是由下而上的(這也是爲何只在init方法中進行createUI不會執行屢次的緣由,由於父類的initWithFrame沒作createUI操做)。
結論: createUI方法最好在initWithFrame中調用,外部使用init或initWithFrame都可以正常執行createUI方法。不要在自定義View中同時重寫init與initWithFrame並執行相同視圖佈局代碼。會致使佈局代碼(createUI)執行屢次。
2、關於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 【<UIView: 0x7f80a9c09590; frame = (0 0; 100 100); layer = <CALayer: 0x600003ff0a40>>】
複製代碼
根據打印結果可驗證,CustomView始終只存在一個子視圖(testView)。
新舊父視圖一致,咱們能夠假設蘋果作了以下處理:
一、在舊父視圖中移除子視圖,再從新將子視圖添加到父視圖上
二、判斷新舊父視圖是否一致,若一致,不作任何操做。
由於沒法看到addSubview的源碼,猜想可能會有這兩種狀況,我的更偏向第二種處理。(可重寫子視圖layoutSubviews方法,由於addSubviews會調用layoutSubviews方法,咱們能夠調試layoutSubviews的調用次數,測試後可驗證addSubviews作了上述二的處理)
結論:若父視圖重複添加同一子視圖,並不會產生多層級狀況。由於此例中testView是以懶加載的形式建立,因此self每次添加的均爲同一個View,但若是在createUI中以UIView *testView = [UIView alloc] initWithFrame的形式建立,那就會建立出多層級的View。
總結:自定義View的子視圖最好以懶加載形式建立,可避免因其餘書寫不當致使的異常
3、關於layoutSubviews
關於這一點,主要想聊一聊layoutSubviews的調用時機
一、setNeedsLayout\ layoutIfNeeded
二、addSubview
三、View的大小發生變化,未變不調用
四、UIScrollView滑動
五、旋轉Screen會觸發父UIView上的layoutSubviews事件
所以對於layoutSubviews的使用咱們須要注意如下幾點:
一、自定義視圖的init方法並不會調用layoutSubviews
二、蘋果聲明不要直接調用layoutSubviews方法,若是須要更新,應該調用setNeedsLayout方法,視圖會在下一次繪製後更新。若是須要當即更新視圖,須要執行layoutIfNeeded方法
三、由於layoutSubviews調用比較頻繁,所以若無特殊需求(文檔所述爲執行精確的子視圖佈局時可以使用),不用重寫layoutSubviews方法。
4、關於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。(不會影響當前視圖,可是會間接影響其子視圖)
好了,暫時寫到這裏,無規矩不成方圓,對於開發者一個好的代碼規範每每能夠事半功倍,共勉!