問題:給一個(gè)UIbutton 添加手勢(shì) 和添加touchUpInside 和 重寫touch:begin方法,誰會(huì)響應(yīng)?
1.UITouch(源起觸摸)
一個(gè)手指一次觸摸屏幕,就對(duì)應(yīng)生成一個(gè)UITouch對(duì)象。多個(gè)手指同時(shí)觸摸,生成多個(gè)UITouch對(duì)象。
多個(gè)手指先后觸摸,系統(tǒng)會(huì)根據(jù)觸摸的位置判斷是否更新同一個(gè)UITouch對(duì)象。若兩個(gè)手指一前一后觸摸同一個(gè)位置(即雙擊),那么第一次觸摸時(shí)生成一個(gè)UITouch對(duì)象,第二次觸摸更新這個(gè)UITouch對(duì)象(UITouch對(duì)象的 tap count 屬性值從1變成2);若兩個(gè)手指一前一后觸摸的位置不同,將會(huì)生成兩個(gè)UITouch對(duì)象,兩者之間沒有聯(lián)系。
每個(gè)UITouch對(duì)象記錄了觸摸的一些信息,包括觸摸時(shí)間、位置、階段、所處的視圖、窗口等信息。
手指離開屏幕一段時(shí)間后,確定該UITouch對(duì)象不會(huì)再被更新將被釋放。
//觸摸的各個(gè)階段狀態(tài)
//例如當(dāng)手指移動(dòng)時(shí),會(huì)更新phase屬性到UITouchPhaseMoved;手指離屏后,更新到UITouchPhaseEnded
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
1.1 UIEvent(事件的真身)
觸摸的目的是生成觸摸事件供響應(yīng)者響應(yīng),一個(gè)觸摸事件對(duì)應(yīng)一個(gè)UIEvent對(duì)象,其中的 type 屬性標(biāo)識(shí)了事件的類型(之前說過事件不只是觸摸事件)。
UIEvent對(duì)象中包含了觸發(fā)該事件的觸摸對(duì)象的集合,因?yàn)橐粋€(gè)觸摸事件可能是由多個(gè)手指同時(shí)觸摸產(chǎn)生的。觸摸對(duì)象集合通過 allTouches 屬性獲取。
1.2 UIResponder
每個(gè)響應(yīng)者都是一個(gè)UIResponder對(duì)象,即所有派生自UIResponder的對(duì)象,本身都具備響應(yīng)事件的能力。因此以下類的實(shí)例都是響應(yīng)者:
UIView
UIViewController
UIApplication
AppDelegate
響應(yīng)者之所以能響應(yīng)事件,因?yàn)槠涮峁┝?個(gè)處理觸摸事件的方法:
//手指觸碰屏幕,觸摸開始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移動(dòng)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指離開屏幕,觸摸結(jié)束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//觸摸結(jié)束前,某個(gè)系統(tǒng)事件中斷了觸摸,例如電話呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
這幾個(gè)方法在響應(yīng)者對(duì)象接收到事件的時(shí)候調(diào)用,用于做出對(duì)事件的響應(yīng)。關(guān)于響應(yīng)者何時(shí)接收到事件以及事件如何沿著響應(yīng)鏈傳遞將在下面章節(jié)說明。
尋找事件的最佳響應(yīng)者(Hit-Testing)
事件自下而上的傳遞
應(yīng)用接收到事件后先將其置入事件隊(duì)列中以等待處理。出隊(duì)后,application首先將事件傳遞給當(dāng)前應(yīng)用最后顯示的窗口(UIWindow)詢問其能否響應(yīng)事件。若窗口能響應(yīng)事件,則傳遞給子視圖詢問是否能響應(yīng),子視圖若能響應(yīng)則繼續(xù)詢問子視圖。子視圖詢問的順序是優(yōu)先詢問后添加的子視圖,即子視圖數(shù)組中靠后的視圖。事件傳遞順序如下:
UIApplication ——> UIWindow ——> 子視圖 ——> ... ——> 子視圖
事實(shí)上把UIWindow也看成是視圖即可,這樣整個(gè)傳遞過程就是一個(gè)遞歸詢問子視圖能否響應(yīng)事件過程,且后添加的子視圖優(yōu)先級(jí)高(對(duì)于window而言就是后顯示的window優(yōu)先級(jí)高)。
2. iOS中事件的產(chǎn)生和傳遞
1.發(fā)生觸摸事件后,系統(tǒng)會(huì)將該事件加入到一個(gè)由UIApplication管理的隊(duì)列事件中
2.UIApplication會(huì)從事件隊(duì)列中取出最前面的事件,并將事件分發(fā)下去以便處理,通常會(huì)先發(fā)送事件給應(yīng)用程序的主窗口(keyWindow)
3.主窗口會(huì)在視圖層次結(jié)構(gòu)中找到一個(gè)最合適的視圖來處理觸摸事件
事件的具體傳遞過程,如圖:

一般事件的傳遞是從父控件傳遞到子控件的
例如:
1.點(diǎn)擊了綠色的View,傳遞過程如下:UIApplication->Window->白色View->綠色View
2.點(diǎn)擊藍(lán)色的View,傳遞過程如下:UIApplication->Window->白色View->橙色View->藍(lán)色View
如果父控件接受不到觸摸事件,那么子控件就不可能接收到觸摸事件
( 重難點(diǎn))如何尋找最合適的view
應(yīng)用如何找到最合適的控件來處理事件?有以下準(zhǔn)則
1.首先判斷主窗口(keyWindow)自己是否能接受觸摸事件
2.觸摸點(diǎn)是否在自己身上
3.從后往前遍歷子控件,重復(fù)前面的兩個(gè)步驟(首先查找數(shù)組中最后一個(gè)元素)
4.如果沒有符合條件的子控件,那么就認(rèn)為自己最合適處理
詳述:
1.主窗口接收到應(yīng)用程序傳遞過來的事件后,首先判斷自己能否接手觸摸事件。如果能,那么在判斷觸摸點(diǎn)在不在窗口自己身上
2.如果觸摸點(diǎn)也在窗口身上,那么窗口會(huì)從后往前遍歷自己的子控件(遍歷自己的子控件只是為了尋找出來最合適的view)
3.遍歷到每一個(gè)子控件后,又會(huì)重復(fù)上面的兩個(gè)步驟(傳遞事件給子控件,1.判斷子控件能否接受事件,2.點(diǎn)在不在子控件上)
4.如此循環(huán)遍歷子控件,直到找到最合適的view,如果沒有更合適的子控件,那么自己就成為最合適的view。
底層具體實(shí)現(xiàn)如下 :
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//3種狀態(tài)無法響應(yīng)事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//觸摸點(diǎn)若不在當(dāng)前視圖上則無法響應(yīng)事件
if ([self pointInside:point withEvent:event] == NO) return nil;
//從后往前遍歷子視圖數(shù)組
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--)
{
// 獲取子視圖
UIView *childView = self.subviews[I];
// 坐標(biāo)系的轉(zhuǎn)換,把觸摸點(diǎn)在當(dāng)前視圖上坐標(biāo)轉(zhuǎn)換為在子視圖上的坐標(biāo)
CGPoint childP = [self convertPoint:point toView:childView];
//詢問子視圖層級(jí)中的最佳響應(yīng)視圖
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView)
{
//如果子視圖中有更合適的就返回
return fitView;
}
}
//沒有在子視圖中找到更合適的響應(yīng)視圖,那么自身就是最合適的
return self;
}
注意:之所以會(huì)采取從后往前遍歷子控件的方式尋找最合適的view只是為了做一些循環(huán)優(yōu)化。因?yàn)橄啾容^之下,后添加的view在上面,降低循環(huán)次數(shù)
UIView不能接收觸摸事件的三種情況:
1.不接受用戶交互:userInteractionEnabled = NO;
2.隱藏:hidden = YES;
3.透明:alpha = 0.0~0.01

說明一下控件的添加順序:白1->綠2->橙2->藍(lán)3->紅3->黃4
這里點(diǎn)擊了橙色的那塊區(qū)域,事件傳遞判斷過程如下:
1.UIApplication從事件隊(duì)列中取出事件分發(fā)給UIWindow
2.UIWindow判斷自己是否能接受觸摸事件,可以
3.UIWindow判斷觸摸點(diǎn)是否在自己身上,是的。
4.UIWindow從后往前便利自己的子控件,取出白1
5.白1都滿足最上面兩個(gè)條件,遍歷子控件橙2
6.橙2都滿足最上面兩個(gè)條件,遍歷子控件,先取出紅3
7.紅3不滿足條件2,取出藍(lán)3
8.藍(lán)3也不滿足條件2,最后最合適的控件是橙2
尋找合適的View用到兩個(gè)重要方法:
hitTest:withEvent:
pointInside
hitTest:withEvent:方法
什么時(shí)候調(diào)用?
只要事件一傳遞給一個(gè)控件,這個(gè)控件就會(huì)調(diào)用他自己的hitTest:withEvent:方法尋找合適的View
作用
尋找并返回最合適的view(能夠響應(yīng)事件的那個(gè)最合適的view)
注 意:不管這個(gè)控件能不能處理事件,也不管觸摸點(diǎn)在不在這個(gè)控件上,
事件都會(huì)先傳遞給這個(gè)控件,隨后再調(diào)用hitTest:withEvent:方法
hitTest:withEvent:底層調(diào)用流程:

事件傳遞給窗口或控件的后,就調(diào)用hitTest:withEvent:方法尋找更合適的view。所以是,先傳遞事件,再根據(jù)事件在自己身上找更合適的view。不管子控件是不是最合適的view,系統(tǒng)默認(rèn)都要先把事件傳遞給子控件,經(jīng)過子控件調(diào)用自己的hitTest:withEvent:方法驗(yàn)證后才知道有沒有更合適的view。即便父控件是最合適的view了,子控件的hitTest:withEvent:方法還是會(huì)調(diào)用,不然怎么知道有沒有更合適的!即,如果確定最終父控件是最合適的view,那么該父控件的子控件的hitTest:withEvent:方法也是會(huì)被調(diào)用的。
3.事件響應(yīng)
這個(gè)過程中,假如應(yīng)用中存在多個(gè)window對(duì)象,UIApplication是怎么知道要把事件傳給哪個(gè)window的?window又是怎么知道哪個(gè)視圖才是最佳響應(yīng)者的呢?
響應(yīng)者對(duì)于事件的操作方式:
響應(yīng)者對(duì)于事件的攔截以及傳遞都是通過 touchesBegan:withEvent: 方法控制的,該方法的默認(rèn)實(shí)現(xiàn)是將事件沿著默認(rèn)的響應(yīng)鏈往下傳遞。
響應(yīng)者對(duì)于接收到的事件有3種操作:
不攔截,默認(rèn)操作
事件會(huì)自動(dòng)沿著默認(rèn)的響應(yīng)鏈往下傳遞
攔截,不再往下分發(fā)事件
重寫 touchesBegan:withEvent: 進(jìn)行事件處理,不調(diào)用父類的 touchesBegan:withEvent:
攔截,繼續(xù)往下分發(fā)事件
重寫 touchesBegan:withEvent: 進(jìn)行事件處理,同時(shí)調(diào)用父類的 touchesBegan:withEvent: 將事件往下傳遞
響應(yīng)鏈中的事件傳遞規(guī)則:
每一個(gè)響應(yīng)者對(duì)象(UIResponder對(duì)象)都有一個(gè) nextResponder 方法,用于獲取響應(yīng)鏈中當(dāng)前對(duì)象的下一個(gè)響應(yīng)者。因此,一旦事件的最佳響應(yīng)者確定了,這個(gè)事件所處的響應(yīng)鏈就確定了。
對(duì)于響應(yīng)者對(duì)象,默認(rèn)的 nextResponder 實(shí)現(xiàn)如下:
UIView
若視圖是控制器的根視圖,則其nextResponder為控制器對(duì)象;否則,其nextResponder為父視圖。
UIViewController
若控制器的視圖是window的根視圖,則其nextResponder為窗口對(duì)象;若控制器是從別的控制器present出來的,則其nextResponder為presenting view controller。
UIWindow
nextResponder為UIApplication對(duì)象。
UIApplication
若當(dāng)前應(yīng)用的app delegate是一個(gè)UIResponder對(duì)象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為app delegate。
上圖是官網(wǎng)對(duì)于響應(yīng)鏈的示例展示,若觸摸發(fā)生在UITextField上,則事件的傳遞順序是:
- UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation
上文介紹了事件的傳遞過程,找到合適的View之后就會(huì)調(diào)用該view的touches方法要進(jìn)行響應(yīng)處理具體的事件,找不到最合適的view,就不會(huì)調(diào)用touches方法進(jìn)行事件處理。
這里先介紹一下響應(yīng)者鏈條:響應(yīng)者鏈條其實(shí)就是很多響應(yīng)者對(duì)象(繼承自UIResponder的對(duì)象)一起組合起來的鏈條稱之為響應(yīng)者鏈條
一般默認(rèn)做法是控件將事件順著響應(yīng)者鏈條向上傳遞,將事件交給上一個(gè)響應(yīng)者進(jìn)行處理 (即調(diào)用super的touches方法)。
那么如何判斷當(dāng)前響應(yīng)者的上一個(gè)響應(yīng)者是誰呢?有以下兩個(gè)規(guī)則:
1.判斷當(dāng)前是否是控制器的View,如果是控制器的View,上一個(gè)響應(yīng)者就是控制器
2.如果不是控制器的View,上一個(gè)響應(yīng)者就是父控件

- 找到最合適的view會(huì)調(diào)用touches方法處理事件
- touches默認(rèn)做法是把事件順著響應(yīng)者鏈條向上拋
//只要點(diǎn)擊控件,就會(huì)調(diào)用touchBegin,如果沒有重寫這個(gè)方法,自己處理不了觸摸事件
// 上一個(gè)響應(yīng)者可能是父控件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 默認(rèn)會(huì)把事件傳遞給上一個(gè)響應(yīng)者,上一個(gè)響應(yīng)者是父控件,交給父控件處理
[super touchesBegan:touches withEvent:event];
// 注意不是調(diào)用父控件的touches方法,而是調(diào)用父類的touches方法
// super是父類 superview是父控件
如果控制器也不響應(yīng)響應(yīng)touches方法,就交給UIWindow。如果UIWindow也不響應(yīng),交給UIApplication,如果都不響應(yīng)事件就作廢了。
用具體的例子來看下響應(yīng)過程,

控件的添加順序:紅1->藍(lán)2->綠2->黃3

綠色View實(shí)現(xiàn)了touch方法,如下

黃色view沒有實(shí)現(xiàn)touch方法,如下

當(dāng)點(diǎn)擊黃色區(qū)域時(shí),由于黃色view沒有實(shí)現(xiàn)touch方法,就順著響應(yīng)鏈找到其父view(綠色View),綠色view實(shí)現(xiàn)了touch方法,便打印了-- touchGreen
最后總結(jié)來說一次完整的觸摸事件的傳遞響應(yīng)過程為:
UIApplication-->UIWindow-->遞歸找到最合適處理的控件-->控件調(diào)用touches方法-->判斷是否實(shí)現(xiàn)touches方法-->沒有實(shí)現(xiàn)默認(rèn)會(huì)將事件傳遞給上一個(gè)響應(yīng)者-->找到上一個(gè)響應(yīng)者-->找不到方法作廢
一句話總結(jié)整個(gè)過程是:觸摸或者點(diǎn)擊一個(gè)控件,然后這個(gè)事件會(huì)從上向下(從父->子)找最合適的view處理,找到這個(gè)view之后看他能不能處理,能就處理,不能就按照事件響應(yīng)鏈向上(從子->父)傳遞給父控件
事件的傳遞和響應(yīng)的區(qū)別:
事件的傳遞是從上到下(父控件到子控件),事件的響應(yīng)是從下到上(順著響應(yīng)者鏈條向上傳遞:子控件到父控件。
4 實(shí)際項(xiàng)目中的應(yīng)用
- 情景1: 點(diǎn)擊子控件,讓父控件響應(yīng)事件;(點(diǎn)擊綠色View,紅色View響應(yīng))
分析:可通過兩種方式實(shí)現(xiàn)
(1)因?yàn)閔itTest:withEvent:方法的作用就是控件接收到事件后,判斷自己是否能處理事件,判斷點(diǎn)在不在自己的坐標(biāo)系上,然后返回最合適的view。所以,我們可以在hitTest:withEvent:方法里面強(qiáng)制返回父控件為最合適的view.
#import "GreenView2.h"
@implementation GreenView2
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return [self superview]; // return nil;
// 此處返回nil也可以。返回nil就相當(dāng)于當(dāng)前的view不是最合適的view
}
@end
讓誰響應(yīng),就直接重寫誰的touchesBegan: withEvent:方法
#import "RedView1.h"
@implementation RedView1
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"-- touchRed touchesBegan");
}
@end
情景2:點(diǎn)擊子控件,父控件和子控件都響應(yīng)事件
#import "GreenView2.h"
@implementation GreenView2
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"-- touchGreen");
[super touchesBegan:touches withEvent:event];
}
擴(kuò)大UIButton的響應(yīng)熱區(qū)
相信大家都遇到小圖button點(diǎn)擊熱區(qū)太小問題,之前我是用UIButton的setImage方法來設(shè)置圖片解決,但是調(diào)起坐標(biāo)就坑了,得各種計(jì)算不說,寫出的代碼還很難看不便于維護(hù),如果我們用用hit-test view的知識(shí)你就能輕松地解決這個(gè)問題。
重載UIButton的-(BOOL)pointInside: withEvent:方法,讓Point即使落在Button的Frame外圍也返回YES。
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
}
CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
CGRect hitTestingBounds = bounds;
if (minimumHitTestWidth > bounds.size.width) {
hitTestingBounds.size.width = minimumHitTestWidth;
hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
}
if (minimumHitTestHeight > bounds.size.height) {
hitTestingBounds.size.height = minimumHitTestHeight;
hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
}
return hitTestingBounds;
}
2、子view超出了父view的bounds響應(yīng)事件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
/**
* 此注釋掉的方法用來判斷點(diǎn)擊是否在父View Bounds內(nèi),
* 如果不在父view內(nèi),就會(huì)直接不會(huì)去其子View中尋找HitTestView,return 返回
*/
// if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
// }
return nil;
}
-
UIResponder、UIGestureRecognizer、UIControl區(qū)別
iOS中,除了UIResponder能夠響應(yīng)事件,手勢(shì)識(shí)別器、UIControl同樣具備對(duì)事件的處理能力。
此處要探討的是:手勢(shì)識(shí)別器與UIResponder的聯(lián)系。
事實(shí)上,手勢(shì)分為離散型手勢(shì)(discrete gestures)和持續(xù)型手勢(shì)(continuous gesture)。系統(tǒng)提供的離散型手勢(shì)包括點(diǎn)按手勢(shì)(UITapGestureRecognizer)和輕掃手勢(shì)(UISwipeGestureRecognizer),其余均為持續(xù)型手勢(shì)。
兩者主要區(qū)別在于狀態(tài)變化過程:
離散型:
識(shí)別成功:Possible —> Recognized
識(shí)別失?。篜ossible —> Failed
持續(xù)型:
完整識(shí)別:Possible —> Began —> [Changed] —> Ended
不完整識(shí)別:Possible —> Began —> [Changed] —> Cancel

控制器的視圖上add了一個(gè)View記為YellowView,并綁定了一個(gè)單擊手勢(shì)識(shí)別器。
// LXFViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(actionTap)];
[self.view addGestureRecognizer:tap];
}
- (void)actionTap{
NSLog(@"View Taped");
}
單擊YellowView,日志打印如下:
-[YellowView touchesBegan:withEvent:]
View Taped
-[YellowView touchesCancelled:withEvent:]
從日志上看出YellowView最后Cancel了對(duì)觸摸事件的響應(yīng),
而正常應(yīng)當(dāng)是觸摸結(jié)束后,YellowView的 touchesEnded:withEvent: 的方法被調(diào)用才對(duì)。另外,期間還執(zhí)行了手勢(shì)識(shí)別器綁定的action 。我從官方文檔找到了這樣的解釋:
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.
大致理解是,Window在將事件傳遞給hit-tested view之前,會(huì)先將事件傳遞給相關(guān)的手勢(shì)識(shí)別器并由手勢(shì)識(shí)別器優(yōu)先識(shí)別。若手勢(shì)識(shí)別器成功識(shí)別了事件,就會(huì)取消hit-tested view對(duì)事件的響應(yīng);若手勢(shì)識(shí)別器沒能識(shí)別事件,hit-tested view才完全接手事件的響應(yīng)權(quán)。
一句話概括:手勢(shì)識(shí)別器比UIResponder具有更高的事件響應(yīng)優(yōu)先級(jí)??!
按照這個(gè)解釋,Window在將事件傳遞給hit-tested view即YellowView之前,先傳遞給了控制器根視圖上的手勢(shì)識(shí)別器。手勢(shì)識(shí)別器成功識(shí)別了該事件,通知Application取消YellowView對(duì)事件的響應(yīng)。
然而看日志,卻是YellowView的
touchesBegan:withEvent:先調(diào)用了,既然手勢(shì)識(shí)別器先響應(yīng),不應(yīng)該上面的action先執(zhí)行嗎?手勢(shì)識(shí)別器的action的調(diào)用時(shí)機(jī)(即此處的
actionTap)并不是手勢(shì)識(shí)別器接收到事件的時(shí)機(jī),而是手勢(shì)識(shí)別器成功識(shí)別事件后的時(shí)機(jī),即手勢(shì)識(shí)別器的狀態(tài)變?yōu)?a target="_blank">UIGestureRecognizerStateRecognized。因此從該日志中并不能看出事件是優(yōu)先傳遞給手勢(shì)識(shí)別器的,那該怎么證明Window先將事件傳遞給了手勢(shì)識(shí)別器?
手勢(shì)識(shí)別器對(duì)于事件的響應(yīng)也是通過這4個(gè)熟悉的方法來實(shí)現(xiàn)的。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
需要注意的是,雖然手勢(shì)識(shí)別器通過這幾個(gè)方法來響應(yīng)事件,但它并不是UIResponder的子類,相關(guān)的方法聲明在 UIGestureRecognizerSubclass.h 中。
- 這樣一來,我們便可以自定義一個(gè)單擊手勢(shì)識(shí)別器的類,重寫這幾個(gè)方法來監(jiān)聽手勢(shì)識(shí)別器接收事件的時(shí)機(jī)。創(chuàng)建一個(gè)UITapGestureRecognizer的子類,重寫響應(yīng)事件的方法,每個(gè)方法中調(diào)用父類的實(shí)現(xiàn),并替換demo中的手勢(shì)識(shí)別器。
另外需要在.m文件中引入 import <UIKit/UIGestureRecognizerSubclass.h> ,因?yàn)橄嚓P(guān)方法聲明在該頭文件中。
// LXFTapGestureRecognizer (繼承自UITapGestureRecognizer)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesCancelled:touches withEvent:event];
}
現(xiàn)在,再次點(diǎn)擊YellowView,日志如下:
-[LXFTapGestureRecognizer touchesBegan:withEvent:]
-[YellowView touchesBegan:withEvent:]
-[LXFTapGestureRecognizer touchesEnded:withEvent:]
View Taped
-[YellowView touchesCancelled:withEvent:]
很明顯,確實(shí)是手勢(shì)識(shí)別器先接收到了事件。之后手勢(shì)識(shí)別器成功識(shí)別了手勢(shì),執(zhí)行了action,再由Application取消了YellowView對(duì)事件的響應(yīng)。
3.Window怎么知道要把事件傳遞給哪些手勢(shì)識(shí)別器?
- 之前探討過Application怎么知道要把event傳遞給哪個(gè)Window,以及Window怎么知道要把event傳遞給哪個(gè)hit-tested view的問題,答案是這些信息都保存在event所綁定的touch對(duì)象上。手勢(shì)識(shí)別器也是一樣的,event綁定的touch對(duì)象上維護(hù)了一個(gè)手勢(shì)識(shí)別器數(shù)組,里面的手勢(shì)識(shí)別器毫無疑問是在hit-testing的過程中收集的。打個(gè)斷點(diǎn)看一下touch上綁定的手勢(shì)識(shí)別器數(shù)組:

Window先將事件傳遞給這些手勢(shì)識(shí)別器,再傳給hit-tested view。一旦有手勢(shì)識(shí)別器成功識(shí)別了手勢(shì),Application就會(huì)取消hit-tested view對(duì)事件的響應(yīng)。
持續(xù)型手勢(shì)
將上面Demo中視圖綁定的單擊手勢(shì)識(shí)別器用滑動(dòng)手勢(shì)識(shí)別器(UIPanGestureRecognizer)替換。
- (void)viewDidLoad {
[super viewDidLoad];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(actionPan)];
[self.view addGestureRecognizer:pan];
}
- (void)actionPan{
NSLog(@"View panned");
}
在YellowView上執(zhí)行一次滑動(dòng):

日志打印如下:
-[YellowView touchesBegan:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesCancelled:withEvent:]
View panned
View panned
View panned
在一開始滑動(dòng)的過程中,手勢(shì)識(shí)別器處在識(shí)別手勢(shì)階段,滑動(dòng)產(chǎn)生的連續(xù)事件既會(huì)傳遞給手勢(shì)識(shí)別器又會(huì)傳遞給YellowView,因此YellowView的 touchesMoved:withEvent: 在開始一段時(shí)間內(nèi)會(huì)持續(xù)調(diào)用;當(dāng)手勢(shì)識(shí)別器成功識(shí)別了該滑動(dòng)手勢(shì)時(shí),手勢(shì)識(shí)別器的action開始調(diào)用,同時(shí)通知Application取消YellowView對(duì)事件的響應(yīng)。之后僅由滑動(dòng)手勢(shì)識(shí)別器接收事件并響應(yīng),YellowView不再接收事件。
另外,在滑動(dòng)的過程中,若手勢(shì)識(shí)別器未能識(shí)別手勢(shì),則事件在觸摸滑動(dòng)過程中會(huì)一直傳遞給hit-tested view-
先總結(jié)一下手勢(shì)識(shí)別器與UIResponder對(duì)于事件響應(yīng)的聯(lián)系:
當(dāng)觸摸發(fā)生或者觸摸的狀態(tài)發(fā)生變化時(shí),Window都會(huì)傳遞事件尋求響應(yīng)。
- Window先將綁定了觸摸對(duì)象的事件傳遞給觸摸對(duì)象上綁定的手勢(shì)識(shí)別器,再發(fā)送給觸摸對(duì)象對(duì)應(yīng)的hit-tested view。
- 手勢(shì)識(shí)別器識(shí)別手勢(shì)期間,若觸摸對(duì)象的觸摸狀態(tài)發(fā)生變化,事件都是先發(fā)送給手勢(shì)識(shí)別器再發(fā)送給hit-test view。
- 手勢(shì)識(shí)別器若成功識(shí)別了手勢(shì),則通知Application取消hit-tested view對(duì)于事件的響應(yīng),并停止向hit-tested view發(fā)送事件;
- 若手勢(shì)識(shí)別器未能識(shí)別手勢(shì),而此時(shí)觸摸并未結(jié)束,則停止向手勢(shì)識(shí)別器發(fā)送事件,僅向hit-test view發(fā)送事件。
- 若手勢(shì)識(shí)別器未能識(shí)別手勢(shì),且此時(shí)觸摸已經(jīng)結(jié)束,則向hit-tested view發(fā)送end狀態(tài)的touch事件以停止對(duì)事件的響應(yīng)。
手勢(shì)識(shí)別器的3個(gè)屬性
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;
- cancelsTouchesInView
默認(rèn)為YES。表示當(dāng)手勢(shì)識(shí)別器成功識(shí)別了手勢(shì)之后,會(huì)通知Application取消響應(yīng)鏈對(duì)事件的響應(yīng),并不再傳遞事件給hit-test view。若設(shè)置成NO,表示手勢(shì)識(shí)別成功后不取消響應(yīng)鏈對(duì)事件的響應(yīng),事件依舊會(huì)傳遞給hit-test view。
demo中設(shè)置: pan.cancelsTouchesInView = NO
滑動(dòng)時(shí)日志:
-[YellowView touchesBegan:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesMoved:withEvent:]
View panned
View panned
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesMoved:withEvent:]
即便滑動(dòng)手勢(shì)識(shí)別器識(shí)別了手勢(shì),Application也會(huì)依舊發(fā)送事件給YellowView。
- delaysTouchesBegan
默認(rèn)為NO。默認(rèn)情況下手勢(shì)識(shí)別器在識(shí)別手勢(shì)期間,當(dāng)觸摸狀態(tài)發(fā)生改變時(shí),Application都會(huì)將事件傳遞給手勢(shì)識(shí)別器和hit-tested view;若設(shè)置成YES,則表示手勢(shì)識(shí)別器在識(shí)別手勢(shì)期間,截?cái)嗍录?,即不?huì)將事件發(fā)送給hit-tested view。
設(shè)置 pan.delaysTouchesBegan = YES
日志如下:
View panned
View panned
View panned
View panned
因?yàn)榛瑒?dòng)手勢(shì)識(shí)別器在識(shí)別期間,事件不會(huì)傳遞給YellowView,因此期間YellowView的 touchesBegan:withEvent: 和 touchesMoved:withEvent: 都不會(huì)被調(diào)用;而后滑動(dòng)手勢(shì)識(shí)別器成功識(shí)別了手勢(shì),也就獨(dú)吞了事件,不會(huì)再傳遞給YellowView。因此只打印了手勢(shì)識(shí)別器成功識(shí)別手勢(shì)后的action調(diào)用。
- delaysTouchesEnded
默認(rèn)為YES。當(dāng)手勢(shì)識(shí)別失敗時(shí),若此時(shí)觸摸已經(jīng)結(jié)束,會(huì)延遲一小段時(shí)間(0.15s)再調(diào)用響應(yīng)者的 touchesEnded:withEvent:;若設(shè)置成NO,則在手勢(shì)識(shí)別失敗時(shí)會(huì)立即通知Application發(fā)送狀態(tài)為end的touch事件給hit-tested view以調(diào)用 touchesEnded:withEvent: 結(jié)束事件響應(yīng)。
總結(jié):手勢(shì)識(shí)別器比響應(yīng)鏈具有更高的事件響應(yīng)優(yōu)先級(jí)。
-
UIControl
- UIControl是系統(tǒng)提供的能夠以target-action模式處理觸摸事件的控件,iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子類。當(dāng)UIControl跟蹤到觸摸事件時(shí),會(huì)向其上添加的target發(fā)送事件以執(zhí)行action。值得注意的是,UIConotrol是UIView的子類,因此本身也具備UIResponder應(yīng)有的身份。
關(guān)于UIControl,此處介紹兩點(diǎn):
target-action執(zhí)行時(shí)機(jī)及過程
觸摸事件優(yōu)先級(jí)
UIControl作為能夠響應(yīng)事件的控件,必然也需要待事件交互符合條件時(shí)才去響應(yīng),因此也會(huì)跟蹤事件發(fā)生的過程。不同于UIResponder以及UIGestureRecognizer通過 touches 系列方法跟蹤,UIControl有其獨(dú)特的跟蹤方式:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;
4個(gè)方法和UIResponder的那4個(gè)方法幾乎吻合,只不過UIControl只能接收單點(diǎn)觸控,因此接收的參數(shù)是單個(gè)UITouch對(duì)象。這幾個(gè)方法的職能也和UIResponder一致,用來跟蹤觸摸的開始、滑動(dòng)、結(jié)束、取消。不過,UIControl本身也是UIResponder,因此同樣有 touches 系列的4個(gè)方法。事實(shí)上,UIControl的 Tracking 系列方法是在 touch 系列方法內(nèi)部調(diào)用的。比如 beginTrackingWithTouch 是在 touchesBegan 方法內(nèi)部調(diào)用的, 因此它雖然也是UIResponder,但 touches 系列方法的默認(rèn)實(shí)現(xiàn)和UIResponder本類還是有區(qū)別的
- 當(dāng)UIControl跟蹤事件的過程中,識(shí)別出事件交互符合響應(yīng)條件,就會(huì)觸發(fā)target-action進(jìn)行響應(yīng)。UIControl控件通過 addTarget:action:forControlEvents: 添加事件處理的target和action,當(dāng)事件發(fā)生時(shí),UIControl通知target執(zhí)行對(duì)應(yīng)的action。說是“通知”其實(shí)很籠統(tǒng),事實(shí)上這里有個(gè)action傳遞的過程。當(dāng)UIControl監(jiān)聽到需要處理的交互事件時(shí),會(huì)調(diào)用 sendAction:to:forEvent: 將target、action以及event對(duì)象發(fā)送給全局應(yīng)用,Application對(duì)象再通過 sendAction:to:from:forEvent: 向target發(fā)送action。
- 因此,可以通過重寫UIControl的 sendAction:to:forEvent: 或 sendAction:to:from:forEvent: 自定義事件執(zhí)行的target及action。
另外,若不指定target,即 addTarget:action:forControlEvents: 時(shí)target傳空,那么當(dāng)事件發(fā)生時(shí),Application會(huì)在響應(yīng)鏈上從上往下尋找能響應(yīng)action的對(duì)象。
觸摸事件優(yōu)先級(jí)
In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer.This applies only to gesture recognition that overlaps the default action for a control, which includes:
A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.
A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.
簡單理解
UIControl會(huì)阻止父視圖上的手勢(shì)識(shí)別器行為,也就是UIControl處理事件的優(yōu)先級(jí)比UIGestureRecognizer高,但前提是相比于父視圖上的手勢(shì)識(shí)別器。
UIControl的響應(yīng)優(yōu)先級(jí)比手勢(shì)識(shí)別器高的說法不準(zhǔn)確,準(zhǔn)確地說只適用于系統(tǒng)提供的有默認(rèn)action操作的UIControl,例如UIbutton、UISwitch等的單擊,而對(duì)于自定義的UIControl,經(jīng)驗(yàn)證,響應(yīng)優(yōu)先級(jí)比手勢(shì)識(shí)別器低

[圖片上傳失敗...(image-90bba7-1554346016683)]
預(yù)置場景:在BlueView上添加一個(gè)button,同時(shí)給button添加一個(gè)target-action事件。
示例一:在BlueView上添加點(diǎn)擊手勢(shì)識(shí)別器
示例二:在button上添加手勢(shì)識(shí)別器
操作方式:單擊button
測試結(jié)果:示例一中,button的target-action響應(yīng)了單擊事件;示例二中,BlueView上的手勢(shì)識(shí)別器響應(yīng)了事件。過程日志打印如下:
//示例一
-[CLTapGestureRecognizer touchesBegan:withEvent:]
-[CLButton touchesBegan:withEvent:]
-[CLButton beginTrackingWithTouch:withEvent:]
-[CLTapGestureRecognizer touchesEnded:withEvent:] after called state = 5
-[CLButton touchesEnded:withEvent:]
-[CLButton endTrackingWithTouch:withEvent:]
按鈕點(diǎn)擊
//示例二
-[CLTapGestureRecognizer touchesBegan:withEvent:]
-[CLButton touchesBegan:withEvent:]
-[CLButton beginTrackingWithTouch:withEvent:]
-[CLTapGestureRecognizer touchesEnded:withEvent:] after called state = 3
手勢(shì)觸發(fā)
-[CLButton touchesCancelled:withEvent:]
-[CLButton cancelTrackingWithEvent:]
原因分析:點(diǎn)擊button后,事件先傳遞給手勢(shì)識(shí)別器,再傳遞給作為hit-tested view存在的button(UIControl本身也是UIResponder,這一過程和普通事件響應(yīng)者無異)。示例一中,由于button阻止了父視圖BlueView中的手勢(shì)識(shí)別器的識(shí)別,導(dǎo)致手勢(shì)識(shí)別器識(shí)別失?。顟B(tài)為failed 枚舉值為5),button完全接手了事件的響應(yīng)權(quán),事件最終由button響應(yīng);示例二中,button未阻止其本身綁定的手勢(shì)識(shí)別器的識(shí)別,因此手勢(shì)識(shí)別器先識(shí)別手勢(shì)并識(shí)別成功(狀態(tài)為ended 枚舉值為3),而后通知Application取消響應(yīng)鏈對(duì)事件的響應(yīng),因?yàn)?touchesCancelled 被調(diào)用,同時(shí) cancelTrackingWithEvent 跟著調(diào)用,因此button的target-action得不到執(zhí)行。
其他:經(jīng)測試,若示例一中的手勢(shì)識(shí)別器設(shè)置 cancelsTouchesInView 為NO,手勢(shì)識(shí)別器和button都能響應(yīng)事件。也就是說這種情況下,button不會(huì)阻止父視圖中手勢(shì)識(shí)別器的識(shí)別
結(jié)論:UIControl比其 父視圖 上的手勢(shì)識(shí)別器具有更高的事件響應(yīng)優(yōu)先級(jí)。
手勢(shì)銷毀? 滑動(dòng)手勢(shì)??

@interface UIScrollViewDelayedTouchesBeganGestureRecognizer : UIGestureRecognizer {
UIView<UIScrollViewDelayedTouchesBeganGestureRecognizerClient> * _client;
struct CGPoint {
float x;
float y;
} _startSceneReferenceLocation;
UIDelayedAction * _touchDelay;
}
- (void).cxx_destruct;
- (id)_clientView;
- (void)_resetGestureRecognizer;
- (void)clearTimer;
- (void)dealloc;
- (void)sendDelayedTouches;
- (void)sendTouchesShouldBeginForDelayedTouches:(id)arg1;
- (void)sendTouchesShouldBeginForTouches:(id)arg1 withEvent:(id)arg2;
- (void)touchesBegan:(id)arg1 withEvent:(id)arg2;
- (void)touchesCancelled:(id)arg1 withEvent:(id)arg2;
- (void)touchesEnded:(id)arg1 withEvent:(id)arg2;
- (void)touchesMoved:(id)arg1 withEvent:(id)arg2;
@end
\\
//TouchEventHook.m
+ (void)load{
Class aClass = objc_getClass("UIScrollViewDelayedTouchesBeganGestureRecognizer");
SEL sel = @selector(hook_sendTouchesShouldBeginForDelayedTouches:);
Method method = class_getClassMethod([self class], sel);
class_addMethod(aClass, sel, class_getMethodImplementation([self class], sel), method_getTypeEncoding(method));
exchangeMethod(aClass, @selector(sendTouchesShouldBeginForDelayedTouches:), sel);
}
- (void)hook_sendTouchesShouldBeginForDelayedTouches:(id)arg1{
[self hook_sendTouchesShouldBeginForDelayedTouches:arg1];
}
void exchangeMethod(Class aClass, SEL oldSEL, SEL newSEL) {
Method oldMethod = class_getInstanceMethod(aClass, oldSEL);
Method newMethod = class_getInstanceMethod(aClass, newSEL);
method_exchangeImplementations(oldMethod, newMethod);
}
現(xiàn)象一由于點(diǎn)擊后,UIScrollViewDelayedTouchesBeganGestureRecognizer 攔截了事件并延遲了0.15s發(fā)送。又因?yàn)辄c(diǎn)擊時(shí)間比0.15s短,在發(fā)送事件前觸摸就結(jié)束了,因此事件沒有傳遞到hit-tested view,導(dǎo)致TableView的 touchBegin 沒有調(diào)用。而現(xiàn)象二,由于短按的時(shí)間超過了0.15s,手勢(shì)識(shí)別器攔截了事件并經(jīng)過0.15s后,觸摸還未結(jié)束,于是將事件傳遞給了hit-tested view,使得TableView接收到了事件。因此現(xiàn)象二的日志雖然和離散型手勢(shì)Demo中的日志一致,但實(shí)際上前者的hit-tested view是在觸摸后延遲了約0.15s左右才接收到觸摸事件的。

]
答案:
現(xiàn)象一 快速點(diǎn)擊cell
backview taped???
現(xiàn)在回到現(xiàn)象一。按照之前的分析,快速點(diǎn)擊cell,講道理不管是表現(xiàn)還是日志都應(yīng)該和現(xiàn)象二一致才對(duì)。然而日志僅僅打印了手勢(shì)識(shí)別器的action執(zhí)行結(jié)果。分析一下原因:GLTableView的 touchesBegan 沒有調(diào)用,說明事件沒有傳遞給hit-tested view。那只有一種可能,就是事件被某個(gè)手勢(shì)識(shí)別器攔截了。目前已知的手勢(shì)識(shí)別器攔截事件的方法,就是設(shè)置 delaysTouchesBegan 為YES,在手勢(shì)識(shí)別器未識(shí)別完成的情況下不會(huì)將事件傳遞給hit-tested view。然后事實(shí)上并沒有進(jìn)行這樣的設(shè)置,那么問題可能出在別的手勢(shì)識(shí)別器上
現(xiàn)象二 短按cell
這個(gè)日志和上面離散型手勢(shì)Demo中打印的日志完全一致。短按后,BackView上的手勢(shì)識(shí)別器先接收到事件,之后事件傳遞給hit-tested view,作為響應(yīng)者鏈中一員的GLTableView的 `touchesBegan:withEvent:` 被調(diào)用;而后手勢(shì)識(shí)別器成功識(shí)別了點(diǎn)擊事件,action執(zhí)行,同時(shí)通知Application取消響應(yīng)鏈中的事件響應(yīng),GLTableView的 `touchesCancelled:withEvent:` 被調(diào)用。
因?yàn)槭录蝗∠?,因此Cell無法響應(yīng)點(diǎn)擊。
-[GLTableView touchesBegan:withEvent:]
backview taped
-[GLTableView touchesCancelled:withEvent:]
現(xiàn)象三 長按cell
-[GLTableView touchesBegan:withEvent:]
-[GLTableView touchesEnded:withEvent:]
cell selected!
長按的過程中,一開始事件同樣被傳遞給手勢(shì)識(shí)別器和hit-tested view,作為響應(yīng)鏈中一員的GLTableView的 touchesBegan:withEvent: 被調(diào)用;此后在長按的過程中,手勢(shì)識(shí)別器一直在識(shí)別手勢(shì),直到一定時(shí)間后手勢(shì)識(shí)別失敗,才將事件的響應(yīng)權(quán)完全交給響應(yīng)鏈。當(dāng)觸摸結(jié)束的時(shí)候,GLTableView的 touchesEnded:withEvent: 被調(diào)用,同時(shí)Cell響應(yīng)了點(diǎn)擊。
現(xiàn)象四 點(diǎn)擊button
-[GLButton touchesBegan:withEvent:]
-[GLButton touchesEnded:withEvent:]
button clicked!