首頁 > 軟體

IOS開發自定義view方法規範範例

2022-07-19 18:00:02

前言

對於接觸業務開發的童鞋,自定義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 【<UIView: 0x7f80a9c09590; frame = (0 0; 100 100); layer = <CALayer: 0x600003ff0a40>>】

根據列印結果可驗證,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。(不會影響當前檢視,但是會間接影響其子檢視)

以上就是IOS開發自定義view方法規範範例的詳細內容,更多關於IOS開發自定義view的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com