你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS自定義控件開發梳理

iOS自定義控件開發梳理

編輯:IOS開發基礎

在日常iOS開發中,系統提供的控件常常無法滿足業務功能,這個時候需要我們實現一些自定義控件。自定義控件能讓我們完全控制視圖的展示內容以及交互操作。本篇將介紹一些自定義控件的相關概念,探討自定義控件開發的基本過程及技巧。

UIView

在開始之前我們先介紹一個類UIVew,它在iOS APP中占有絕對重要的地位,因為幾乎所有的控件都是繼承自UIView類。

UIView表示屏幕上的一個矩形區域,負責渲染區域內的內容,並且響應區域內發生的觸摸事件。

在UIView的內部有一個CALayer,提供內容的繪制和顯示,包括UIView的尺寸樣式。UIView的frame實際上返回的CALayer的frame。

UIView繼承自UIResponder類,它能接收並處理從系統傳來的事件,CALayer繼承自NSObject,它無法響應事件。所以UIView與CALayer的最大區別在於:UIView能響應事件,而CALayer不能。

更詳細的資料:

https://developer.apple.com/reference/uikit/uiview

http://www.cocoachina.com/ios/20150828/13244.html

兩種實現方式

在創建自定義控件時,主要有兩種實現方式,分別是純代碼以及xib。接下來我們用這兩種方式分別演示一下創建自定義控件的步驟。

我們實現一個簡單的demo ,效果如下,封裝一個圓形的imageView。

1.png

使用代碼創建自定義控件

使用代碼創建自定義控件,首先創建一個繼承自UIView的類

2.png

實現initWithFrame:方法。在該方法中,設置自定義控件的屬性,並創建、添加子視圖:

-(instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
        _imageView.contentMode = UIViewContentModeScaleAspectFill;
        _imageView.layer.masksToBounds = YES;
        _imageView.layer.cornerRadius = frame.size.width/2;
        [self addSubview:_imageView];
    }
    return self;
}

如果需要對子視圖重新布局,需要調用layoutSubViews方法:

-(void)layoutSubviews {

    [super layoutSubviews];

    _imageView.frame = self.frame;

    _imageView.layer.cornerRadius = self.frame.size.width/2;

}

layoutSubviews是調整子視圖布局的方法,官方文檔如下

You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want.

意思是當你需要調整subview的大小的時候,重寫layoutSubviews方法。

layoutSubviews在以下情況下會被調用:

1、init初始化不會觸發layoutSubviews

2、addSubview會觸發layoutSubviews

3、設置view的Frame會觸發layoutSubviews,當然前提是frame的值設置前後發生了變化

4、滾動一個UIScrollView會觸發layoutSubviews

5、旋轉Screen會觸發父UIView上的layoutSubviews事件

6、改變一個UIView大小的時候也會觸發父UIView上的layoutSubviews事件

這個自定義控件提供對外接口方法,為自定義的控件賦值

- (void)configeWithImage:(UIImage *)image {
    _imageView.image = image;
}

最後,添加自定義控件到頁面上

    _circleImageView = [[CircleImageView alloc] initWithFrame:CGRectMake(0, 80, 150, 150)];
    [_circleImageView configeWithImage:[UIImage imageNamed:@"tree"]];
    [self.view addSubview:_circleImageView];

運行效果

3.png

通過xib創建自定義控件

4.png

首先創建一個自定義控件XibCircleImageView,繼承自UIView

創建xib文件,與XibCircleImageView類同名

5.png

配置xib中imageView的屬性,並將XibCircleImageView 類與對應的xib文件進行綁定

6.png

代碼如下

- (void)awakeFromNib {
    [super awakeFromNib];
    _imageView.layer.masksToBounds = YES;
    _imageView.layer.cornerRadius = self.frame.size.width/2;
    [self addSubview:_imageView];
}
- (void)configeWithImage:(UIImage *)image {
    _imageView.image = image;
}
-(void)layoutSubviews {
    [super layoutSubviews];
    _imageView.layer.cornerRadius = self.frame.size.width/2;
}

在頁面中調用方式有點不同,通過loadNibNamed方法創建xib對象

    //使用xib創建自定義控件

_xibCircleImageView = [[[NSBundle mainBundle] loadNibNamed:@"XibCircleImageView" owner:nil options:nil] lastObject];
    _xibCircleImageView.frame = CGRectMake(0, 500, 100, 100);
    [_xibCircleImageView configeWithImage:image];
    [self.view addSubview:_xibCircleImageView];

當使用xib創建自定義控件時,初始化不會調用initWithFrame:方法,只會調用initWithCoder:方法,初始化完畢後才調用awakeFromNib方法,注意要在awakeFromNib中初始化子控件。因為initWithCoder:方法表示對象是從文件解析來的,就會調用,而awakeFromNib方法是從xib或者storyboard加載完畢後才會調用。

本文demo地址:https://github.com/superzcj/ZCJCustomViewDemo

小結

這兩種創建自定義控件的方式各有優劣,純代碼方式比較靈活,維護和擴展都比較方便,但寫起來比較麻煩。xib方式開發效率高,但不易擴展和維護,適合功能樣式比較穩定的自定義控件。

事件傳遞機制

在自定義控件中,可能需要動態響應事件,如按鈕太小,不易點擊,需要擴大按鈕的點擊范圍,接下來我們談談iOS的事件傳遞機制。

事件響應鏈

UIResponder類能夠響應觸摸、手勢以及遠程控制等事件。它是所有可響應事件的基類,其中包括很常見的UIView、UIViewController以及UIApplication。

UIResponder的屬性和方法如下圖,其中nextResponder表示指向一個UIResponder對象。

8.png

那麼事件響應鏈與UIResponder有什麼關系呢?應用內的視圖按一定的結構組織起來,即樹狀層次結構,一個視圖可以有多個子視圖,而子視圖只能有一個父視圖。當一個視圖被添加到父視圖上時。每一個視圖的nextResponder屬性就指向它的父視圖,這樣,整個應用就通過nextResponder串成了一條鏈,即響應鏈。響應鏈是一個虛擬鏈,並不是真實存在的,它借助UIResponder的nextResponder串連起來。如下圖

7.png

Hit-Test View

有了事件響應鏈,接下來就是尋找具體響應對象了,我們稱之為:Hit-Testing View,尋找這個View的過程稱為Hit-Test。

什麼是Hit-Test?我們可以把它理解為一個探測器,通過這個探測器,我們可以找到並判斷手指是否觸摸在某個視圖上。

Hit-Test是如何工作的?Hit-Test采用遞歸方式從視圖的根節點開始遍歷,直到找到某個點擊的視圖。

首先從UIWindow發送hitTest:withEvent:消息開始,判斷該視圖是否能響應觸摸事件,如果不能響應返回nil,表示該視圖不能響應觸摸事件。然後再調用pointInside:withEvent:方法,該方法用於判斷觸摸事件點擊的位置是否處理該視圖范圍內,如果pointInside:withEvent:返回no,那麼hitTest:withEvent:也直接返回nil。

如果pointInside:withEvent: 方法返回yes,那麼該視圖向所有子視圖發送hitTest:withEvent:消息,所有子視圖的調用順序是從最頂層視圖一直到最底層視圖,即從subViews的數組的末尾向前遍歷。直到有子視圖返回非空對象或全部遍歷完畢。若有子視圖返回非空對象,則hitTest:withEvent:方法返回該對象,處理結束;若所有子視圖都返回nil,則hitTest:withEvent:方法返回該視圖自身。

事件傳遞機制的應用

舉幾個例子,說明一下事件傳遞機制在自定義控件中的應用。

一、擴大view的點擊區域。假設一個button的大小為20px 20px,太小難以點擊。我們通過重寫這個button子類的hitTest:withEvent:方法,判斷點擊處point是否在button周圍20px以內,如果是則返回自身,實現擴大點擊范圍的功能,代碼如下:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.hidden || self.alpha<=0.01) {
        return nil;
    }
    CGRect touchRect = CGRectInset(self.bounds, -20, -20);
    if (CGRectContainsPoint(touchRect, point)) {
        for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subView convertPoint:point toView:self];
            UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

二、穿透傳遞事件。

假設有兩個view,viewA和viewB,viewB完全覆蓋viewA,我們希望點擊viewB時能響應viewA的事件。我們重寫這個viewA的hitTest:withEvent:方法,不繼續遍歷它的子視圖,直接返回自身。代碼如下:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.hidden || self.alpha<=0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        NSLog(@"in view A");
        return self;
    }
    return nil;
}

更詳細的資料:

http://zhoon.github.io/ios/2015/04/12/ios-event.html

http://www.jianshu.com/p/2f664e71c527

回調機制

在自定義控件開發中,需要向它的父類回傳返回值。比如一個存放按鈕的自定義控件,需要在上層接收按鈕點擊事件。我們可以使用多種方式回調消息,比如target action模式、代理、block、通知等。

Target-Action

Target-Action是一種設計模式,當事件觸發時,它讓一個對象向另一個對象發送消息。這個模式我們接觸的比較多,如為按鈕綁定點擊事件,為view添加手勢事件等。UIControl及其子類都支持這個機制。Target-Action 在消息的發送者和接收者之間建立了一個松散的關系。消息的接收者不知道發送者,甚至消息的發送者也不知道消息的接收者會是什麼。

基於 target-action 傳遞機制的一個局限是,發送的消息不能攜帶自定義的信息。iOS 中,可以選擇性的把發送者和觸發 action 的事件作為參數。除此之外就沒有別的控制 action 消息內容的方法了。

舉個例子,我們使用Target-Action為控件添加一個單擊手勢。

        UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(refresh)];
        [_imageView addGestureRecognizer:tapGR];
- (void)refresh{
    NSLog(@"Touch imageView");
}

代理

代理是一種我們常用的回調方式,也是蘋果推薦的方式,在系統框架UIKit中大量使用,如UITableView、UITextField。

優點:

  1. 代理語法清晰,可讀性高,易於維護 ;

  2. 它減少了代碼耦合性,使事件監聽與事件處理分離;

  3. 一個控制器可以實現多個代理,滿足自定義開發需求,靈活性較高;

缺點:

  1. 實現代理的過程較繁瑣;

  2. 跨層傳值時加大代碼的耦合性,並且程序的層次結構也變得混亂;

  3. 當多個對象同時傳值時不易區分,導致代理易用性大大降低;

Block

Block封裝一段代碼,並當做變量進行傳遞,它十分方便地將不同地方的代碼組織在一起,可讀性很高。

優點:1,語法簡潔,代碼可讀性和可維護性較高。2,配合GCD優秀的解決多純程問題。

缺點:1,Block中得代碼將自動進行一次retain操作,容易造成內存洩露。 2.Block內默認引用為強引用,容易造成循環引用。

通知

代理是一對一的關系,通知是一對多的關系,通知相比代理可以實現更大跨度的通信機制。但接收對象多了,就難以控制,有時不希望的對象也接收處理了消息。

優點:

  1. 使用簡單,代碼精簡。

  2. 支持一對多,解決了同時向多個對象監聽的問題。

  3. 傳值方便快捷,Context自身攜帶相應的內容。

缺點:

  1. 通知使用完畢後需要注銷,否則會造成意外崩潰。

  2. key不夠安全,編譯器不會檢測到是否被通知中心正確處理。

  3. 調試時難以跟蹤。

  4.  當使用者向通知中心發送通知的時候,並不能獲得任何反饋信息。 

  5. 需要一個第三方的對象來做監聽者與被監聽者的中介。

更詳細的資料:

https://objccn.io/issue-3-4/

http://maru-zhang.tk/2015/06/08/iOS-Development-Delegate,Notification,Block/

總結

至此,開發自定義控件的相關知識梳理了一遍,希望能幫助大家更好地理解自定義控件開發。


  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved