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

iOS自定義控件教程(一)UIKit入門,布局入門
iOS自定義控件教程(二)響應(yīng)鏈原理
iOS自定義控件教程(三)觸摸事件和簡單動畫
iOS自定義控件教程(四)Target-Action響應(yīng)模式

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

Github下載源碼

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

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

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

  1. 程序一運行,所有的類方法(‘+’開頭)和實例方法(‘-’開頭)的接口內(nèi)存地址都被寫入一張hash表中
  2. 我們向UIView發(fā)送類方法alloc消息,runtime(運行時環(huán)境)根據(jù)前面說的hash表,查找對應(yīng)類(UIView)的對應(yīng)類方法(alloc)的內(nèi)存地址,然后調(diào)用
  3. 如果UIView并未實現(xiàn)alloc方法,runtime會轉(zhuǎn)而查找UIView的父類是否實現(xiàn)了alloc方法,如果實現(xiàn)了,就將消息投遞給父類的alloc方法;如果沒有實現(xiàn),轉(zhuǎn)而查找UIView父類的父類是否實現(xiàn),重復(fù)這一過程直到將消息投遞出去
  4. 如果最終發(fā)現(xiàn)投遞不出去,則會拋出一個最常見的異常unrecognized selector sent to instance + 內(nèi)存地址,也就是你調(diào)用了一個沒有實現(xiàn)的方法

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

需求 我們需要將DemoXXXSegmentView獲取的觸摸事件,反饋給當前的UIViewContoller,應(yīng)該怎么做?

1. 直接調(diào)用

我們從最蠢的做法說起,雖然是蠢,但是是可行的,不過不要模仿啊,單純?yōu)榱酥v原理和作對照

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

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

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

2. delegate(委托)模式

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

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

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

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

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

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

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

3. Target模式

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

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

UIControliOS的人機交互制定了一系列的標準:

例如最常見的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定義了控件的基本狀態(tài)

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;

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

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

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

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

給個提示:

- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents;                        // send all actions associated with events

4. block(塊語法)

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

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

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

ViewContoller中,我們給XXXSegmentViewdidSelectBlock賦值

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

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

然后在XXXSegmentView中加入block調(diào)用

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    
    //.....其他代碼
    
    if (self.didSelectBlock) {
        self.didSelectBlock(touchNumber);
    }
}

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

小結(jié)

最終效果

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

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

XXXRoundMenu.gif

iOS自定義控件教程(一)UIKit入門,布局入門
iOS自定義控件教程(二)響應(yīng)鏈原理
iOS自定義控件教程(三)觸摸事件和簡單動畫
iOS自定義控件教程(四)Target-Action響應(yīng)模式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,667評論 4 61
  • 18歲,花一樣的年紀,本該張揚的似正午的太陽,耀眼炙熱。 我的18歲雖然不炙熱,但是也有夢想,也有悄悄的關(guān)注喜歡的...
    蘇涼1991閱讀 258評論 0 1

友情鏈接更多精彩內(nèi)容