iOS自定義控件教程(一)UIKit入門,布局入門
iOS自定義控件教程(二)響應(yīng)鏈原理
iOS自定義控件教程(三)觸摸事件和簡單動畫
iOS自定義控件教程(四)Target-Action響應(yīng)模式
上一篇文章我們介紹了UIView的觸摸事件響應(yīng)和簡單動畫,但是并沒有將觸摸事件封裝。我們今天介紹Demo中最后一部分 —— 輸出響應(yīng)事件。
我么知道Objective-C是采用消息機制(messaging)調(diào)用方法的,例如我們調(diào)用UIView的init方法
UIView * simpleView = [[UIView alloc] init];
簡單的描述一下其中的過程:
- 程序一運行,所有的類方法(‘+’開頭)和實例方法(‘-’開頭)的接口內(nèi)存地址都被寫入一張hash表中
- 我們向
UIView發(fā)送類方法alloc消息,runtime(運行時環(huán)境)根據(jù)前面說的hash表,查找對應(yīng)類(UIView)的對應(yīng)類方法(alloc)的內(nèi)存地址,然后調(diào)用 - 如果UIView并未實現(xiàn)alloc方法,runtime會轉(zhuǎn)而查找UIView的父類是否實現(xiàn)了alloc方法,如果實現(xiàn)了,就將消息投遞給父類的alloc方法;如果沒有實現(xiàn),轉(zhuǎn)而查找UIView父類的父類是否實現(xiàn),重復(fù)這一過程直到將消息投遞出去
- 如果最終發(fā)現(xiàn)投遞不出去,則會拋出一個最常見的異常
unrecognized selector sent to instance + 內(nèi)存地址,也就是你調(diào)用了一個沒有實現(xiàn)的方法
不過,我們今天遇到的問題單單依靠消息機制并不能很好的解決。
需求 我們需要將Demo中XXXSegmentView獲取的觸摸事件,反饋給當前的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)用ViewController的segmentDidSelectIdx方法,傳遞選擇標簽這個事件。
這樣是可行的,但是缺點十分明顯:耦合性太高,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作為XXXSegmentView的delegate屬性傳入,XXXSegmentView無需知道自己的delegate是什么類,便可以直接調(diào)用delegate的實例方法。
if (self.delegate && [self.delegate respondsToSelector:@selector(segmentDidSelectIdx:)]) {
[self.delegate segmentDidSelectIdx:idx];
}
注意我們在調(diào)用之前,首先檢查self的delegate是否賦值,然后通過respondsToSelector確認delegate實現(xiàn)了segmentDidSelectIdx方法,最后才傳遞消息。這兩步十分重要,delegate作為動態(tài)類型,編譯器編譯階段是無法發(fā)現(xiàn)問題的,所以運行時要進行確認。
注: 標準的委托模式是要結(jié)合協(xié)議(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定義了控件的基本狀態(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中,我們給XXXSegmentView的didSelectBlock賦值
@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

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