iOS自定義控件教程(四)UIControl - 幕后的英雄
上一篇文章我們介紹了UIView的觸摸事件響應和簡單動畫,但是并沒有將觸摸事件封裝。我們今天介紹Demo中最后一部分 —— 輸出響應事件。
我么知道 Objective-C 是采用 消息機制 (messaging)調用方法的,例如我們調用 UIView 的 init 方法
UIView * simpleView = [[UIView alloc] init];
簡單的描述一下其中的過程:
-
程序一運行,所有的類方法(‘+’開頭)和實例方法(‘-’開頭)的接口內存地址都被寫入一張hash表中
-
我們向 UIView 發送類方法 alloc 消息,runtime(運行時環境)根據前面說的hash表,查找對應類(UIView)的對應類方法(alloc)的內存地址,然后調用
-
如果UIView并未實現alloc方法,runtime會轉而查找UIView的父類是否實現了alloc方法,如果實現了,就將消息投遞給父類的alloc方法;如果沒有實現,轉而查找UIView父類的父類是否實現,重復這一過程直到將消息投遞出去
-
如果最終發現投遞不出去,則會拋出一個最常見的異常 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