iOS自定義控件教程(四)UIControl - 幕后的英雄

jopen 8年前發布 | 13K 次閱讀 UIControl iOS開發 移動開發

上一篇文章我們介紹了UIView的觸摸事件響應和簡單動畫,但是并沒有將觸摸事件封裝。我們今天介紹Demo中最后一部分 —— 輸出響應事件。

Github下載源碼

我么知道 Objective-C 是采用 消息機制 (messaging)調用方法的,例如我們調用 UIView 的 init 方法

UIView * simpleView = [[UIView alloc] init];

簡單的描述一下其中的過程:

  1. 程序一運行,所有的類方法(‘+’開頭)和實例方法(‘-’開頭)的接口內存地址都被寫入一張hash表中

  2. 我們向 UIView 發送類方法 alloc 消息,runtime(運行時環境)根據前面說的hash表,查找對應類(UIView)的對應類方法(alloc)的內存地址,然后調用

  3. 如果UIView并未實現alloc方法,runtime會轉而查找UIView的父類是否實現了alloc方法,如果實現了,就將消息投遞給父類的alloc方法;如果沒有實現,轉而查找UIView父類的父類是否實現,重復這一過程直到將消息投遞出去

  4. 如果最終發現投遞不出去,則會拋出一個最常見的異常 unrecognized selector sent to instance + 內存地址 ,也就是你調用了一個沒有實現的方法

不過,我們今天遇到的問題單單依靠 消息機制 并不能很好的解決。

需求我們需要將 Demo 中 XXXSegmentView 獲取的觸摸事件,反饋給當前的UIViewContoller,應該怎么做?

1. 直接調用

我們從最蠢的做法說起,雖然是蠢,但是是可行的,不過不要模仿啊,單純為了講原理和作對照

@interface ViewController ()
@property (strong, nonatomic) XXXSegmentView *segmentView;
- (void)segmentDidSelectIdx:(NSInteger)idx;
@end
@interface XXXSegmentView : UIView
@property (weak, nonatomic) ViewController *viewController;
@end

我們在給 XXXSegmentView 加上一個 viewController 屬性,然后就可以在獲取觸摸事件時,通過調用 ViewController 的 segmentDidSelectIdx 方法,傳遞選擇標簽這個事件。

這樣是可行的,但是缺點十分明顯:耦合性太高, XXXSegmentView 需要引用 ViewController 頭文件,不符合低耦合這個基本原則。

2. delegate(委托)模式

objc 的delegate設計模式,可以解決上面的問題。但根據 objc 的設計初衷,這個問題用delegate解決真的有種殺雞用牛刀的感覺。

@interface XXXSegmentView : UIView
@property (nonatomic, weak) id delegate;
@end

這里的delegate屬性,是一個 id 類型的屬性, id 這個類型就是 objc 的動態類型,編譯器不關心它是什么類型,所以 id 類型的對象,可以調用所有聲明過的類方法和實例方法,而編譯器不會報錯。

這樣我們就可以個把 viewController 作為 XXXSegmentView 的 delegate 屬性傳入, XXXSegmentView 無需知道自己的 delegate 是什么類,便可以直接調用 delegate 的實例方法。

if (self.delegate && [self.delegate respondsToSelector:@selector(segmentDidSelectIdx:)]) {
        [self.delegate segmentDidSelectIdx:idx];
}

注意我們在調用之前,首先檢查 self 的 delegate 是否賦值,然后通過 respondsToSelector 確認 delegate 實現了 segmentDidSelectIdx 方法,最后才傳遞消息。這兩步十分重要, delegate 作為動態類型,編譯器編譯階段是無法發現問題的,所以運行時要進行確認。

注:標準的委托模式是要結合協議(Protocol)一起使用的,這里就不多講了。

3. Target模式

這要從一個類說起,他叫 UIControl 。

UIControl 是 UIView 的子類,是 UIKit 框架中可交互的控件的基類,一般不直接使用。我們用的比較多的例如 UIButton 、 UISwitch 、 UITextField 等都是他的子類。

UIControl 為 iOS 的人機交互制定了一系列的標準:

例如最常見的 UIControlEvents 枚舉,定義了 iOS 交互中的交互方式

typedef NS_OPTIONS(NSUInteger, UIControlEvents) {
    UIControlEventTouchDown                                         = 1 <<  0,      // on all touch downs
    UIControlEventTouchDownRepeat                                   = 1 <<  1,      // on multiple touchdowns (tap count > 1)
    UIControlEventTouchDragInside                                   = 1 <<  2,
    UIControlEventTouchDragOutside                                  = 1 <<  3,
    UIControlEventTouchDragEnter                                    = 1 <<  4,
    UIControlEventTouchDragExit                                     = 1 <<  5,
    UIControlEventTouchUpInside                                     = 1 <<  6,
    UIControlEventTouchUpOutside                                    = 1 <<  7,
    UIControlEventTouchCancel                                       = 1 <<  8,

    UIControlEventValueChanged                                      = 1 << 12,     // sliders, etc.
    UIControlEventPrimaryActionTriggered NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 13,     // semantic action: for buttons, etc.

    UIControlEventEditingDidBegin                                   = 1 << 16,     // UITextField
    UIControlEventEditingChanged                                    = 1 << 17,
    UIControlEventEditingDidEnd                                     = 1 << 18,
    UIControlEventEditingDidEndOnExit                               = 1 << 19,     // 'return key' ending editing

    UIControlEventAllTouchEvents                                    = 0x00000FFF,  // for touch events
    UIControlEventAllEditingEvents                                  = 0x000F0000,  // for UITextField
    UIControlEventApplicationReserved                               = 0x0F000000,  // range available for application use
    UIControlEventSystemReserved                                    = 0xF0000000,  // range reserved for internal framework use
    UIControlEventAllEvents                                         = 0xFFFFFFFF
};

又例如 UIControlState 定義了控件的基本狀態

typedef NS_OPTIONS(NSUInteger, UIControlState) {
    UIControlStateNormal       = 0,
    UIControlStateHighlighted  = 1 << 0,                  // used when UIControl isHighlighted is set
    UIControlStateDisabled     = 1 << 1,
    UIControlStateSelected     = 1 << 2,                  // flag usable by app (see below)
    UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 3, // Applicable only when the screen supports focus
    UIControlStateApplication  = 0x00FF0000,              // additional flags available for application use
    UIControlStateReserved     = 0xFF000000               // flags reserved for internal framework use
};

同時提供了給控件反饋交互操作的一系列方法,例如我們今天要講的

- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;

比如我們有一個按鈕,當他點擊時候,我們執行ViewContollr的 -(void)click:(id)sender 方法,可以這么寫:

UIButton* button = [UIButton buttonWithType:UIButtonTypeSystem];
[button addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];

這里傳入的 UIControlEventTouchUpInside 枚舉量,就是在控件frame內按下,然后抬起這樣一個事件, UIContol 將這個事件作為key,和目標(target)和目標方法(action)存到了自己私有的字典里。當用戶點擊按鈕時, UIControl 響應了觸摸鏈的 touchesEnded 方法,便會根據私有字典,把對應 UIControlEventTouchUpInside 的目標(target)和目標方法(action)調用,這樣完成事件的回傳。

這是一個很好的設計,從原則上講,我們的 XXXSegmentView 是一個可交互控件,理應繼承于 UIControl 而非 UIView ,但筆者偷懶了,讀者有興趣可以自己嘗試改寫。

4. block(塊語法)

沒有繼承 UIControl ,筆者只好祭出終極大殺器, block 。block語法特性加入iOS已經有段日子了,因為使用方法篇幅太大,這里就不細說了,推薦一篇相關 教程

我們知道block是可以當作對象看待的,所以給 XXXSegmentView 添加下面這個屬性

@property (nonatomic, strong) void (^ didSelectBlock)(NSUInteger idx);

在 ViewContoller 中,我們給 XXXSegmentView 的 didSelectBlock 賦值

@property (weak, nonatomic) IBOutlet XXXSegmentView *segment;

[segment setDidSelectBlock:^(NSUInteger idx) {
        NSLog(@"segment select %@",@(idx));
}];

然后在 XXXSegmentView 中加入 block 調用

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];

    //.....其他代碼

    if (self.didSelectBlock) {
        self.didSelectBlock(touchNumber);
    }
}

block的調用方法類似C語言的方法調用,傳參格式也相同,注意使用前也要進行非空檢測哦。

小結

至此,我們自制UIKit控件的第一篇教程就結束了,感興趣的朋友可以從 Github下載源碼 對照分析。這幾篇教程主要針對一些有objc基礎,但UIKit剛入門的初學者,希望能幫到你們。

最后跟大家分享一個最的最新作品: zsy78191/XXXRoundMenuButton

來自: http://segmentfault.com/a/1190000004258988

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!