iOS事件的傳遞與響應(yīng)是一個(gè)重要的話題,網(wǎng)上談?wù)摰暮芏?,但大多講述并不完整,本文將結(jié)合蘋果官方的文檔對(duì)事件的傳遞與響應(yīng)原理及應(yīng)用實(shí)踐做一個(gè)比較完整的總結(jié)。文章將依次介紹下列內(nèi)容:
- 事件的傳遞機(jī)制
- 事件的響應(yīng)機(jī)制
- 事件傳遞與響應(yīng)實(shí)踐
- 手勢(shì)識(shí)別器工作機(jī)制
iOS中事件一共有四種類型,包含觸摸事件,運(yùn)動(dòng)事件,遠(yuǎn)程控制事件,按壓事件,本文將只討論最常用的觸摸事件。事件通過UIEvent對(duì)象描述
UIEvent
UIEvent描述了單次的用戶與應(yīng)用的交互行為,例如觸摸屏幕會(huì)產(chǎn)生觸摸事件,晃動(dòng)手機(jī)會(huì)產(chǎn)生運(yùn)動(dòng)事件。UIEvent對(duì)象中記錄了事件發(fā)生的時(shí)間,類型,對(duì)于觸摸事件,還記錄了一組UITouch對(duì)象,下面是UIEvent的幾個(gè)屬性:
@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0); //事件的類型
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) NSTimeInterval timestamp; //事件的時(shí)間
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches; //事件包含的touch對(duì)象
那么觸摸事件中的UITouch對(duì)象描述的是什么呢?
UITouch
UITouch記錄了手指在屏幕上觸摸時(shí)產(chǎn)生的一組信息,包含觸摸的時(shí)間,位置,所在的窗口或視圖,觸摸的狀態(tài),力度等信息
@property(nonatomic,readonly) NSTimeInterval timestamp; //時(shí)間
@property(nonatomic,readonly) UITouchPhase phase; //狀態(tài),例如begin,move,end,cancel
@property(nonatomic,readonly) NSUInteger tapCount; // 短時(shí)間內(nèi)單擊的次數(shù)
@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0); //類型
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0); //觸摸半徑
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);
@property(nullable,nonatomic,readonly,strong) UIWindow *window; //觸摸所在窗口
@property(nullable,nonatomic,readonly,strong) UIView *view; //觸摸所在視圖
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2); //正在接收該觸摸對(duì)象的手勢(shì)識(shí)別器
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0); //觸摸的力度
每一根手指的觸摸都會(huì)產(chǎn)生一個(gè)UITouch對(duì)象,多個(gè)手指觸摸便會(huì)有多個(gè)UITouch對(duì)象,當(dāng)手指在屏幕上移動(dòng)時(shí),系統(tǒng)會(huì)更新UITouch的部分屬性值,在觸摸結(jié)束后系統(tǒng)會(huì)釋放UITouch對(duì)象。
當(dāng)事件產(chǎn)生后,系統(tǒng)會(huì)尋找可以響應(yīng)該事件的對(duì)象來處理事件,如果找不到可以響應(yīng)的對(duì)象,事件就會(huì)被丟棄。那么哪些對(duì)象可以響應(yīng)事件呢?只有繼承于UIResponder的對(duì)象才能夠響應(yīng)事件,UIApplication,UIView,UIViewcontroller均繼承于UIResponder,因此它們能夠響應(yīng)事件。UIResponder提供了響應(yīng)事件的一組方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //手指觸摸到屏幕
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //手指在屏幕上移動(dòng)或按壓
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //手指離開屏幕
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; //觸摸被中斷,例如觸摸時(shí)電話呼入
如果我們想要對(duì)事件進(jìn)行自定義的處理(比如手指在屏幕滑動(dòng)時(shí)讓某個(gè)view跟著移動(dòng)),我們需要重寫以上四個(gè)方法,對(duì)于UIViewcontroller,我們只需要在UIViewcontroller中重寫上面四個(gè)方法,對(duì)于UIView,我們需要?jiǎng)?chuàng)建繼承于UIView的子類,然后在子類中重寫上面的方法,這點(diǎn)需要注意
事件的傳遞
事件產(chǎn)生之后,會(huì)被加入到由UIApplication管理的事件隊(duì)列里,接下來開始自UIApplication往下傳遞,首先會(huì)傳遞給主window,然后按照view的層級(jí)結(jié)構(gòu)一層層往下傳遞,一直找到最合適的view(發(fā)生touch的那個(gè)view)來處理事件。查找最合適的view的過程是一個(gè)遞歸的過程,其中涉及到兩個(gè)重要的方法 hitTest:withEvent:和pointInside:withEvent:
當(dāng)事件傳遞給某個(gè)view之后,會(huì)調(diào)用view的hitTest:withEvent:方法,該方法會(huì)遞歸查找view的所有子view,其中是否有最合適的view來處理事件,整個(gè)流程如下所示:

hitTest:withEvent:代碼實(shí)現(xiàn):
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//首先判斷是否可以接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//然后判斷點(diǎn)是否在當(dāng)前視圖上
if ([self pointInside:point withEvent:event] == NO) return nil;
//循環(huán)遍歷所有子視圖,查找是否有最合適的視圖
for (NSInteger i = self.subviews.count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
//轉(zhuǎn)換點(diǎn)到子視圖坐標(biāo)系上
CGPoint childPoint = [self convertPoint:point toView:childView];
//遞歸查找是否存在最合適的view
UIView *fitView = [childView hitTest:childPoint withEvent:event];
//如果返回非空,說明子視圖中找到了最合適的view,那么返回它
if (fitView) {
return fitView;
}
}
//循環(huán)結(jié)束,仍舊沒有合適的子視圖可以處理事件,那么就認(rèn)為自己是最合適的view
return self;
}
-
pointInside:withEvent:方法作用是判斷點(diǎn)是否在視圖內(nèi),是則返回YES,否則返回NO - 判斷一個(gè)view是否能夠接收事件有三個(gè)條件,分別是,是否禁止用戶交互(userInteractionEnabled = NO),是否被隱藏(hidden = YES)以及透明度是否小于等于0.01(alpha <=0.01)
- 從遞歸的邏輯我們知道,如果觸摸的點(diǎn)不在父view上,那么其上的所有子view的hitTest都不會(huì)被調(diào)用,需要指出的是,如果子view尺寸超出了父view,并且屬性clipsToBounds設(shè)置為NO,觸摸發(fā)生在子view超出父view的區(qū)域內(nèi),依舊不返回子view。反過來,如果觸摸的點(diǎn)在父view上并且父view就是最合適的view,那么它的所有子view的hitTest還是會(huì)被調(diào)用,因?yàn)槿绻徽{(diào)用無法知道是否還有比父view更合適的子view存在。
事件的響應(yīng)
在找到最合適的view之后,會(huì)調(diào)用view的touches方法對(duì)事件進(jìn)行響應(yīng),如果沒有重寫view的touches方法,touches默認(rèn)的做法是將事件沿著響應(yīng)者鏈往上拋,交給下一個(gè)響應(yīng)者對(duì)象。也就是說,touches方法默認(rèn)不處理事件,只是將事件沿著響應(yīng)者鏈往上傳遞。那么響應(yīng)者鏈?zhǔn)鞘裁茨兀?/p>
響應(yīng)者鏈
在應(yīng)用程序中,視圖放置都是有一定層次關(guān)系的,點(diǎn)擊屏幕之后該由下方的哪個(gè)view來響應(yīng)需要有一個(gè)判斷的方式。響應(yīng)者鏈?zhǔn)怯梢幌盗锌梢皂憫?yīng)事件的對(duì)象(繼承于UIResponder)組成的,它決定了響應(yīng)者對(duì)象響應(yīng)事件的先后順序關(guān)系。下圖展示了UIApplication,UIViewcontroller以及UIView之間的響應(yīng)關(guān)系鏈:

響應(yīng)者鏈在遞歸查找最合適的view的時(shí)候形成,所找到的view將成為第一響應(yīng)者,會(huì)調(diào)用它的touches方法來響應(yīng)事件,touches方法默認(rèn)的處理是將事件往上拋給下一個(gè)響應(yīng)者,而如果下一個(gè)響應(yīng)者的touches方法沒有重寫,事件會(huì)繼續(xù)沿著響應(yīng)者鏈往上走,一直到UIApplication,如果依舊不能處理事件那么事件就被丟棄。
-
UIView
如果view是viewcontroller的根view,那么下一個(gè)響應(yīng)者是viewcontroller,否則是super view -
UIViewcontroller
如果viewcontroller的view是window的根view,那么下一個(gè)響應(yīng)者是window;如果viewcontroller是另一個(gè)viewcontroller模態(tài)推出的,那么下一個(gè)響應(yīng)者是另一個(gè)viewcontroller;如果viewcontroller的view被add到另一個(gè)viewcontroller的根view上,那么下一個(gè)響應(yīng)者是另一個(gè)viewcontroller的根view -
UIWindow
UIWindow的下一個(gè)響應(yīng)者是UIApplication -
UIApplication
通常UIApplication是響應(yīng)者鏈的頂端(如果app delegate也繼承了UIResponder,事件還會(huì)繼續(xù)傳給app delegate)
事件傳遞與響應(yīng)實(shí)踐
首先我們通過代碼創(chuàng)建一個(gè)具有層次結(jié)構(gòu)的視圖集合,在viewcontroller的viewDidLoad中添加如下代碼:
greenView *green = [[greenView alloc] initWithFrame:CGRectMake(50, 50, 300, 500)];
[self.view addSubview:green];
redView *red = [[redView alloc] initWithFrame:CGRectMake(0, 0, 200, 300)];
[green addSubview:red];
orangeView *orange = [[orangeView alloc] initWithFrame:CGRectMake(0, 350, 200, 100)];
[green addSubview:orange];
blueView *blue = [[blueView alloc] initWithFrame:CGRectMake(10, 10, 100, 100)];
[red addSubview:blue];
執(zhí)行后如下所示:

要實(shí)現(xiàn)我們自定義的事件處理邏輯,通常有兩種方式,我們可以重寫hitTest:withEvent:方法指定最合適處理事件的視圖,即響應(yīng)鏈的第一響應(yīng)者,也可以通過重寫touches方法來決定該由響應(yīng)鏈上的誰來響應(yīng)事件。
-
情景1:點(diǎn)擊黃色視圖,紅色視圖響應(yīng)
黃色視圖和紅色視圖均為綠色視圖的子視圖,我們可以重寫綠色視圖的hitTest:withEvent:方法,在其中直接返回紅色視圖,代碼示例如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
if ([self pointInside:point withEvent:event] == NO) return nil;
//紅色視圖是先被add的,所以是第一個(gè)元素
return self.subviews[0];
}
我們這里是重寫了父視圖的hitTest方法,而不是重寫紅色視圖的hitTest方法并讓它返回自身,道理也很顯然,在遍歷綠色視圖所有子視圖的過程中,可能還沒來得及調(diào)用到紅色視圖的hitTest方法時(shí),就已經(jīng)遍歷到了觸摸點(diǎn)真正所在的綠色視圖,這個(gè)時(shí)候重寫紅色視圖的hitTest方法是無效的。
-
情景2:點(diǎn)擊紅色視圖,綠色視圖響應(yīng)(也就是事件透?jìng)鳎?/strong>
我們可以重寫紅色視圖的hitTest方法,讓其返回空,這時(shí)候便沒有了合適的子視圖來響應(yīng)事件,父視圖即綠色視圖就成為了最合適的響應(yīng)事件的視圖,代碼示例如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return nil;
}
當(dāng)然,我們也可以重寫綠色視圖的hitTest方法,讓其直接返回自身,也能實(shí)現(xiàn)同樣效果,不過這樣的話點(diǎn)擊其它子視圖(比如黃色視圖)就也不能響應(yīng)事件了,因此如何處理需要視情況而定。
-
情景3:點(diǎn)擊紅色視圖,紅色和綠色視圖均做響應(yīng)
我們知道,事件在不能被處理時(shí),會(huì)沿著響應(yīng)者鏈傳遞給下一個(gè)響應(yīng)者,因此我們可以重寫響應(yīng)者對(duì)象的touches方法來實(shí)現(xiàn)讓一個(gè)事件多個(gè)響應(yīng)者對(duì)象響應(yīng)的目的。因此我們可以通過重寫紅色視圖的touches方法,先做自己的處理,然后在把事件傳遞給下一個(gè)響應(yīng)者,代碼示例如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"red touches begin"); //自己的處理
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"red touches moved"); //自己的處理
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"red touches end"); //自己的處理
[super touchesBegan:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"red touches canceled"); //自己的處理
[super touchesBegan:touches withEvent:event];
}
需要說明的是,事件傳遞給下一個(gè)響應(yīng)者時(shí),用的是super而不是superview,這并沒有問題,因?yàn)閟uper調(diào)用了父類的實(shí)現(xiàn),而父類默認(rèn)的實(shí)現(xiàn)就是調(diào)用下一個(gè)響應(yīng)者的touches方法。如果直接調(diào)用superview反而會(huì)有問題,因?yàn)橄乱粋€(gè)響應(yīng)者可能是viewcontroller
手勢(shì)識(shí)別器
事實(shí)上,我們要處理事件除了使用前面提到的方式,還有另一種方式,就是手勢(shì)識(shí)別器。手勢(shì)識(shí)別器可以很方便的處理常用的各種觸摸事件,常見的手勢(shì)包括單擊、拖動(dòng),長(zhǎng)按,橫掃或豎掃,縮放,旋轉(zhuǎn)等,另外我們還可以創(chuàng)建自定義的手勢(shì)。
UIGestureRecognize是手勢(shì)識(shí)別器的父類,所有具體的手機(jī)識(shí)別器均繼承于該父類,如果我們自定義手勢(shì),也需要繼承該類。該類并沒有繼承于UIResponder,所以手勢(shì)識(shí)別器并不參與響應(yīng)者鏈。那么手勢(shì)識(shí)別器是如何工作的呢?
手勢(shì)識(shí)別器工作機(jī)制
當(dāng)觸摸屏幕產(chǎn)生touch事件后,UIApplication會(huì)將事件往下分發(fā),如果視圖綁定了手勢(shì)識(shí)別器,那么touch事件會(huì)優(yōu)先傳遞給綁定在視圖上的手勢(shì)識(shí)別器,然后手勢(shì)識(shí)別器會(huì)對(duì)手勢(shì)進(jìn)行識(shí)別,如果識(shí)別出了手勢(shì),就會(huì)調(diào)用創(chuàng)建手勢(shì)時(shí)所綁定的回調(diào)方法,并且會(huì)取消將touch事件繼續(xù)傳遞給其所綁定的視圖,如果手勢(shì)識(shí)別器沒有識(shí)別出對(duì)應(yīng)的手勢(shì),那么touch事件會(huì)繼續(xù)向手勢(shì)識(shí)別器所綁定的視圖傳遞。
雖然手勢(shì)識(shí)別器并不是響應(yīng)者鏈中的一員,但是手勢(shì)識(shí)別器會(huì)觀察touch事件,并延遲事件向所綁定的視圖傳遞,這短暫的延遲使手勢(shì)識(shí)別器有機(jī)會(huì)優(yōu)先去識(shí)別手勢(shì)處理touch事件。
對(duì)于UIKit提供的的標(biāo)準(zhǔn)控件,可以很方便地通過Target-Action的方式增加事件處理邏輯,默認(rèn)情況下,發(fā)生在標(biāo)準(zhǔn)控件上的touch事件會(huì)優(yōu)先被標(biāo)準(zhǔn)控件通過target-action方式處理,而不會(huì)去響應(yīng)手勢(shì)。舉個(gè)例子,如果視圖上綁定了單擊的手勢(shì)識(shí)別器,然后視圖上又添加了一個(gè)UIButton,button通過target-action的方式設(shè)置了點(diǎn)擊執(zhí)行的操作,那么當(dāng)點(diǎn)擊button時(shí),響應(yīng)的是button的點(diǎn)擊事件,而不是父視圖上的單擊手勢(shì)。如果希望手勢(shì)識(shí)別器優(yōu)先標(biāo)準(zhǔn)控件的target-action進(jìn)行事件處理,那么可以直接在標(biāo)準(zhǔn)控件上綁定手勢(shì)識(shí)別器,比如上例,如果直接在button上綁定了單擊手勢(shì),那么響應(yīng)的就是單擊手勢(shì)了