深刻理解 iOS 事件機制

前言

這篇文章始於一個需求:咱們在 SDK 提供的某個 View 的 Subview 中實現了單擊雙擊等多個 Gesture Recognizer,而客戶但願本身在這個 View 上的單擊手勢不會衝突,同時沒有延遲。html

藉此機會,咱們來重溫下 iOS 的事件機制和手勢衝突,重點介紹下 UIGestureRecognizer 之間以及與原生觸摸事件的相互關係。node

事件的生命週期

當指尖觸碰屏幕時,一個觸摸事件就在系統中生成了。通過 IPC 進程間通訊,事件最終被傳遞到了合適的應用。在應用內歷經峯迴路轉的奇幻之旅後,最終被釋放。大體通過以下圖:ios

系統響應階段

  1. 手指觸碰屏幕,屏幕感應到觸碰後,將事件交由 IOKit 處理。git

  2. IOKit 將觸摸事件封裝成一個 IOHIDEvent 對象,並經過 mach port 傳遞給 SpringBoard 進程。github

mach port 進程端口,各進程之間經過它進行通訊。數組

SpringBoad.app 是一個系統進程,能夠理解爲桌面系統,能夠統一管理和分發系統接收到的觸摸事件。數據結構

  1. SpringBoard 進程因接收到觸摸事件,觸發了主線程 runloop 的 source1 事件源的回調。此時 SpringBoard 會根據當前桌面的狀態,判斷應該由誰處理這次觸摸事件。由於事件發生時,你可能正在桌面上翻頁,也可能正在刷微博。如果前者(即前臺無 APP 運行),則觸發 SpringBoard 自己主線程 runloop 的 source0 事件源的回調,將事件交由桌面系統去消耗;如果後者(即有 APP 正在前臺運行),則將觸摸事件經過 IPC 傳遞給前臺 APP 進程,接下來的事情即是 APP 內部對於觸摸事件的響應了。

APP響應階段

  1. APP 進程的 mach port 接受到 SpringBoard 進程傳遞來的觸摸事件,主線程的 runloop 被喚醒,觸發了 source1 回調。app

  2. source1 回調又觸發了一個 source0 回調,將接收到的 IOHIDEvent 對象封裝成 UIEvent 對象,此時 APP 將正式開始對於觸摸事件的響應。異步

  3. source0 回調內部將觸摸事件添加到 UIApplication 對象的事件隊列中。事件出隊後,UIApplication 開始一個尋找最佳響應者的過程,這個過程又稱 Hit-Testing,細節將在下一節闡述。另外,此處開始即是與咱們平時開發相關的工做了。ide

  4. 尋找到最佳響應者後,接下來的事情即是事件在響應鏈中的傳遞及響應了。事實上,事件除了被響應者消耗,還能被手勢識別器或是 Target-Action 模式捕捉並消耗掉。其中涉及對觸摸事件的響應優先級。

  5. 觸摸事件歷經坎坷後要麼被某個響應對象捕獲後釋放,要麼至死也沒能找到可以響應的對象,最終釋放。至此,這個觸摸事件的使命就算終結了。runloop 若沒有其餘事件須要處理,也將重歸於眠,等待新的事件到來後喚醒。

探測鏈與響應鏈

Hit-Testing

從邏輯上來講,探測鏈是最早發生的機制,當觸摸事件發生後,iOS 系統根據 Hit-Testing 來肯定觸摸事件發生在哪一個視圖對象上。其中主要用到了兩個 UIView 中的方法:

// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;

// default returns YES if point is in bounds
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
複製代碼

前者會經過遞歸調用後者來返回一個適合響應觸摸事件的視圖,下面這張圖描述了這個過程:

Responder Chain

Hit-Testing 找到的視圖擁有最早對觸摸事件進行處理的機會,若是該視圖沒法處理這個事件,那麼事件對象就會沿着響應器的視圖鏈向上傳遞,直到找到能夠處理該事件的對象爲止。下面這張圖描述了這個過程:

Demo 驗證

接下來咱們經過官方文檔的 Demo 以代碼的方式來進行驗證:

對於每一個 View,咱們重載父類的方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"進入A_View---hitTest withEvent ---");
    UIView * view = [super hitTest:point withEvent:event];
    NSLog(@"離開A_View--- hitTest withEvent ---hitTestView:%@",view);
    return view;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event
{
    NSLog(@"A_view--- pointInside withEvent ---");
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"A_view--- pointInside withEvent --- isInside:%d",isInside);
    return isInside;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"A_touchesBegan");
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
    NSLog(@"A_touchesMoved");
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
    NSLog(@"A_touchesEnded");
    [super touchesEnded:touches withEvent:event];
}

-(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"A_touchesCancelled");
    [super touchesCancelled:touches withEvent:event];
}
複製代碼

點擊 View D,log 顯示以下,這與探測鏈與響應鏈的機制的描述相同。

進入A_View---hitTest withEvent ---
A_view--- pointInside withEvent ---
A_view--- pointInside withEvent --- isInside:1
進入C_View---hitTest withEvent ---
C_view---pointInside withEvent ---
C_view---pointInside withEvent --- isInside:1
進入E_View---hitTest withEvent ---
E_view---pointInside withEvent ---
E_view---pointInside withEvent --- isInside:0
離開E_View---hitTest withEvent ---hitTestView:(null)
進入D_View---hitTest withEvent ---
D_view---pointInside withEvent ---
D_view---pointInside withEvent --- isInside:1
離開D_View---hitTest withEvent ---hitTestView:<DView: 0x12dd11e50; frame = (0 37; 240 61); autoresize = RM+BM; layer = <CALayer: 0x283f87b40>>
離開C_View---hitTest withEvent ---hitTestView:<DView: 0x12dd11e50; frame = (0 37; 240 61); autoresize = RM+BM; layer = <CALayer: 0x283f87b40>>
離開A_View--- hitTest withEvent ---hitTestView:<DView: 0x12dd11e50; frame = (0 37; 240 61); autoresize = RM+BM; layer = <CALayer: 0x283f87b40>>
進入A_View---hitTest withEvent ---
A_view--- pointInside withEvent ---
A_view--- pointInside withEvent --- isInside:1
進入C_View---hitTest withEvent ---
C_view---pointInside withEvent ---
C_view---pointInside withEvent --- isInside:1
進入E_View---hitTest withEvent ---
E_view---pointInside withEvent ---
E_view---pointInside withEvent --- isInside:0
離開E_View---hitTest withEvent ---hitTestView:(null)
進入D_View---hitTest withEvent ---
D_view---pointInside withEvent ---
D_view---pointInside withEvent --- isInside:1
離開D_View---hitTest withEvent ---hitTestView:<DView: 0x12dd11e50; frame = (0 37; 240 61); autoresize = RM+BM; layer = <CALayer: 0x283f87b40>>
離開C_View---hitTest withEvent ---hitTestView:<DView: 0x12dd11e50; frame = (0 37; 240 61); autoresize = RM+BM; layer = <CALayer: 0x283f87b40>>
離開A_View--- hitTest withEvent ---hitTestView:<DView: 0x12dd11e50; frame = (0 37; 240 61); autoresize = RM+BM; layer = <CALayer: 0x283f87b40>>
D_touchesBegan
C_touchesBegan
A_touchesBegan
D_touchesEnded
C_touchesEnded
A_touchesEnded
複製代碼

(這裏其實 Hit-Testing 進行了兩次,關於這個問題,蘋果官方有相應的回覆)

Yes, it’s normal. The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine.

小結

1. 系統經過 hitTest:withEvent: 方法沿視圖層級樹從底向上(從根視圖開始)從後向前(從邏輯上更靠近屏幕的視圖開始)進行遍歷,最終返回一個適合響應觸摸事件的 View。

2. 原生觸摸事件從 Hit-Testing 返回的 View 開始,沿着響應鏈從上向下進行傳遞。

探測鏈與響應鏈的機制整體比較清晰,再也不贅述,但熟悉這兩個機制並不能幫咱們解決任何問題,接下來咱們繼續深刻探究下手勢識別器。

手勢識別器

咱們首先思考一個問題,對於官方文檔裏的 Demo,咱們在每一個 View 上添加一個 UITapGestureRecognizer,當點擊 View D 時,UITapGestureRecognizer 之間的響應順序是什麼樣的,哪一個 View 上的 UITapGestureRecognizer 又會最終響應這個事件?

官方文檔

咱們先來看看官方文檔是怎麼說的:

When a view has multiple gesture recognizers attached to it, you may want to alter how the competing gesture recognizers receive and analyze touch events. By default, there is no set order for which gesture recognizers receive a touch first, and for this reason touches can be passed to gesture recognizers in a different order each time. You can override this default behavior to:

  • Specify that one gesture recognizer should analyze a touch before another gesture recognizer.

  • Allow two gesture recognizers to operate simultaneously.

  • Prevent a gesture recognizer from analyzing a touch.

Use the UIGestureRecognizer class methods, delegate methods, and methods overridden by subclasses to effect these behaviors.

根據文檔的說法,當觸摸事件發生時,哪一個 UIGestureRecognizer 先收到這個事件並無固定的順序,而且文檔建議咱們使用 UIGestureRecognizer 提供的方法來控制它們之間的順序和相互關係。

UIGestureRecognizer Methods

因此咱們依次看下系統的 UIGestureRecognizer 都提供了哪些與它們之間相互關係有關的方法:

// create a relationship with another gesture recognizer that will prevent this gesture's actions from being called until otherGestureRecognizer transitions to UIGestureRecognizerStateFailed
// if otherGestureRecognizer transitions to UIGestureRecognizerStateRecognized or UIGestureRecognizerStateBegan then this recognizer will instead transition to UIGestureRecognizerStateFailed
// example usage: a single tap may require a double tap to fail
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;

// called once per attempt to recognize, so failure requirements can be determined lazily and may be set up between recognizers across view hierarchies
// return YES to set up a dynamic failure requirement between gestureRecognizer and otherGestureRecognizer
//
// note: returning YES is guaranteed to set up the failure requirement. returning NO does not guarantee that there will not be a failure requirement as the other gesture's counterpart delegate or subclass methods may return YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
複製代碼

這三個方法比較經常使用,它們能夠指定 UIGestureRecognizer 之間的依賴關係,區別在於第一個通常適用於在同一個 View 中建立的多個 UIGestureRecognizer 的場景,當 View 層級比較複雜或者 UIGestureRecognizer 處於 Framework 內部時能夠用後兩個方法動態指定。

// called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
// return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
//
// note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
複製代碼

這個方法能夠控制兩個 UIGestureRecognizer 之間是否能夠同時異步進行,須要注意的是,假設存在兩個可能會互相 block 的 UIGestureRecognizer,系統會分別對它們的 delegate 調用這個方法,只要有一個返回 YES,那麼這兩個 UIGestureRecognizer 就能夠同時進行識別,這與 shouldRequireFailureOfGestureRecognizer 是相似的。

// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;

// called when a gesture recognizer attempts to transition out of UIGestureRecognizerStatePossible. returning NO causes it to transition to UIGestureRecognizerStateFailed
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
複製代碼

這兩個方法都是用來禁止 UIGestureRecognizer 響應觸摸事件的,區別在於,當觸摸事件發生時,使用第一個方法能夠當即控制 UIGestureRecognizer 是否對其處理,且不會修改 UIGestureRecognizer 的狀態機(由於在調用自身的 touchesBegan:withEvent: 以前,詳見下),而第二個方法會等待一段時間,在 UIGestureRecognizer 識別手勢轉換狀態時調用,返回 NO 會改變其狀態機,使其 state 變爲 UIGestureRecognizerStateFailed

咱們看下官方文檔對這兩個方法的說明:

When a touch begins, if you can immediately determine whether or not your gesture recognizer should consider that touch, use thegestureRecognizer:shouldReceiveTouch: method. This method is called every time there is a new touch. Returning NO prevents the gesture recognizer from being notified that a touch occurred. The default value is YES. This method does not alter the state of the gesture recognizer.

If you need to wait as long as possible before deciding whether or not a gesture recognizer should analyze a touch, use thegestureRecognizerShouldBegin: delegate method. Generally, you use this method if you have a UIView or UIControl subclass with custom touch-event handling that competes with a gesture recognizer. Returning NO causes the gesture recognizer to immediately fail, which allows the other touch handling to proceed. This method is called when a gesture recognizer attempts to transition out of the Possible state, if the gesture recognition would prevent a view or control from receiving a touch.

You can use the gestureRecognizerShouldBegin:UIView method if your view or view controller cannot be the gesture recognizer’s delegate. The method signature and implementation is the same.

第二段介紹了一般狀況下,當咱們的子類 UIView 或 UIControl 有和 UIGestureRecognizer 衝突的自定義觸摸事件時,可使用 gestureRecognizerShouldBegin: 方法讓 UIGestureRecognizer 失效來使自定義的觸摸事件進行響應。第三段說明了當咱們的 View 不是 UIGestureRecognizer 的 delegate 時,可使用 UIView 中的 gestureRecognizerShouldBegin: 方法。關於這兩段的意思咱們會在後兩節去詳細解釋。

// mirror of the touch-delivery methods on UIResponder
// UIGestureRecognizers aren't in the responder chain, but observe touches hit-tested to their view and their view's subviews
// UIGestureRecognizers receive touches before the view to which the touch was hit-tested
- (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;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
複製代碼

與 UIResponder 中的觸摸事件相關的方法相同,UIGestureRecognizer 有一套本身的觸摸事件的方法,區別在於,UIGestureRecognizer 並不在響應鏈中,這些方法通常是寫用來對特定的手勢進行判斷和識別的邏輯,例如咱們能夠在子類中重寫這些方法來建立本身的 UIGestureRecognizer。使用 gestureRecognizer:shouldReceiveTouch: 可讓這些方法不被調用。

至此,UIGestureRecognizer 已經爲咱們提供了足夠多的方法來控制它們之間的相互關係了,咱們接下來在 Demo 中試試看。

Demo 驗證

對於官方文檔中的 Demo 的每一個 View,咱們增長一個繼承自 UITapGestureRecognizer 的 ZTTapGestureRecognizer 並實現相應的回調:

- (void)singleTapGesture
{
    NSLog(@"A_singleTapGesture");
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    NSLog(@"A_view--- gestureRecognizerShouldBegin: %@ ---", gestureRecognizer.name);
    return YES;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    NSLog(@"A_view--- gestureRecognizer shouldReceiveTouch: %@ ---", gestureRecognizer.name);
    return YES;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    NSLog(@"A_view--- gestureRecognizer: %@ otherGestureRecognizer: %@ ---", gestureRecognizer.name, otherGestureRecognizer.name);
    return YES;
}

- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    
    if (self)
    {
        ZTTapGestureRecognizer *tapGestureRecognizer = [[ZTTapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTapGesture)];
        tapGestureRecognizer.name = @"A_view_tapGestureRecognizer";
        tapGestureRecognizer.delegate = self;
        [self addGestureRecognizer:tapGestureRecognizer];
    }
    
    return self;
}
複製代碼

在咱們子類 ZTTapGestureRecognizer 中重寫父類關於觸摸事件的方法:

@implementation ZTTapGestureRecognizer

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@_touchesBegan", self.name);
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@_touchesMoved", self.name);
    [super touchesMoved:touches withEvent:event];
}

// NSLog 要寫在 super 後面來讀取 state
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    NSLog(@"%@_touchesEndedWithState: %d", self.name, (int)self.state);
}

// NSLog 要寫在 super 後面來讀取 state
-(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesCancelled:touches withEvent:event];
    NSLog(@"%@_touchesCancelledWithState: %d", self.name, (int)self.state);
}

@end
複製代碼

點擊 View D,log 顯示以下:

進入A_View---hitTest withEvent ---
A_view--- pointInside withEvent ---
A_view--- pointInside withEvent --- isInside:1
進入C_View---hitTest withEvent ---
C_view---pointInside withEvent ---
C_view---pointInside withEvent --- isInside:1
進入E_View---hitTest withEvent ---
E_view---pointInside withEvent ---
E_view---pointInside withEvent --- isInside:0
離開E_View---hitTest withEvent ---hitTestView:(null)
進入D_View---hitTest withEvent ---
D_view---pointInside withEvent ---
D_view---pointInside withEvent --- isInside:1
離開D_View---hitTest withEvent ---hitTestView:<DView: 0x104d31050; frame = (0 37; 240 61); autoresize = RM+BM; gestureRecognizers = <NSArray: 0x281b77fc0>; layer = <CALayer: 0x2815e6ca0>>
離開C_View---hitTest withEvent ---hitTestView:<DView: 0x104d31050; frame = (0 37; 240 61); autoresize = RM+BM; gestureRecognizers = <NSArray: 0x281b77fc0>; layer = <CALayer: 0x2815e6ca0>>
離開A_View--- hitTest withEvent ---hitTestView:<DView: 0x104d31050; frame = (0 37; 240 61); autoresize = RM+BM; gestureRecognizers = <NSArray: 0x281b77fc0>; layer = <CALayer: 0x2815e6ca0>>
進入A_View---hitTest withEvent ---
A_view--- pointInside withEvent ---
A_view--- pointInside withEvent --- isInside:1
進入C_View---hitTest withEvent ---
C_view---pointInside withEvent ---
C_view---pointInside withEvent --- isInside:1
進入E_View---hitTest withEvent ---
E_view---pointInside withEvent ---
E_view---pointInside withEvent --- isInside:0
離開E_View---hitTest withEvent ---hitTestView:(null)
進入D_View---hitTest withEvent ---
D_view---pointInside withEvent ---
D_view---pointInside withEvent --- isInside:1
離開D_View---hitTest withEvent ---hitTestView:<DView: 0x104d31050; frame = (0 37; 240 61); autoresize = RM+BM; gestureRecognizers = <NSArray: 0x281b77fc0>; layer = <CALayer: 0x2815e6ca0>>
離開C_View---hitTest withEvent ---hitTestView:<DView: 0x104d31050; frame = (0 37; 240 61); autoresize = RM+BM; gestureRecognizers = <NSArray: 0x281b77fc0>; layer = <CALayer: 0x2815e6ca0>>
離開A_View--- hitTest withEvent ---hitTestView:<DView: 0x104d31050; frame = (0 37; 240 61); autoresize = RM+BM; gestureRecognizers = <NSArray: 0x281b77fc0>; layer = <CALayer: 0x2815e6ca0>>
D_view--- gestureRecognizer shouldReceiveTouch: D_view_tapGestureRecognizer ---
C_view--- gestureRecognizer shouldReceiveTouch: C_view_tapGestureRecognizer ---
A_view--- gestureRecognizer shouldReceiveTouch: A_view_tapGestureRecognizer ---
D_view_tapGestureRecognizer_touchesBegan
A_view_tapGestureRecognizer_touchesBegan
C_view_tapGestureRecognizer_touchesBegan
D_touchesBegan
C_touchesBegan
A_touchesBegan
D_view--- gestureRecognizerShouldBegin: D_view_tapGestureRecognizer ---
D_view_tapGestureRecognizer_touchesEndedWithState: 3
D_view--- gestureRecognizerShouldBegin: A_view_tapGestureRecognizer ---
A_view--- gestureRecognizerShouldBegin: A_view_tapGestureRecognizer ---
A_view_tapGestureRecognizer_touchesEndedWithState: 3
D_view--- gestureRecognizerShouldBegin: C_view_tapGestureRecognizer ---
C_view--- gestureRecognizerShouldBegin: C_view_tapGestureRecognizer ---
C_view_tapGestureRecognizer_touchesEndedWithState: 3
A_view--- gestureRecognizer: A_view_tapGestureRecognizer otherGestureRecognizer: (null) ---
C_view--- gestureRecognizer: C_view_tapGestureRecognizer otherGestureRecognizer: (null) ---
D_view--- gestureRecognizer: D_view_tapGestureRecognizer otherGestureRecognizer: (null) ---
A_view--- gestureRecognizer: A_view_tapGestureRecognizer otherGestureRecognizer: C_view_tapGestureRecognizer ---
A_view--- gestureRecognizer: A_view_tapGestureRecognizer otherGestureRecognizer: D_view_tapGestureRecognizer ---
C_view--- gestureRecognizer: C_view_tapGestureRecognizer otherGestureRecognizer: A_view_tapGestureRecognizer ---
C_view--- gestureRecognizer: C_view_tapGestureRecognizer otherGestureRecognizer: D_view_tapGestureRecognizer ---
D_view--- gestureRecognizer: D_view_tapGestureRecognizer otherGestureRecognizer: A_view_tapGestureRecognizer ---
D_view--- gestureRecognizer: D_view_tapGestureRecognizer otherGestureRecognizer: C_view_tapGestureRecognizer ---
A_singleTapGesture
D_touchesCancelled
C_touchesCancelled
A_touchesCancelled
C_singleTapGesture
D_singleTapGesture
複製代碼

信息量有點大,咱們一點一點來分析(先忽略 View 響應鏈裏 UIResponder 相關的觸摸事件方法,這些會在下一節進行探討),首先系統經過 Hit-Testing 機制找到了適合響應的 View D,接下來調用了方法:

D_view--- gestureRecognizer shouldReceiveTouch: D_view_tapGestureRecognizer ---
C_view--- gestureRecognizer shouldReceiveTouch: C_view_tapGestureRecognizer ---
A_view--- gestureRecognizer shouldReceiveTouch: A_view_tapGestureRecognizer ---
複製代碼

上文已經對 gestureRecognizer:shouldReceiveTouch: 解釋過,先調用它是沒有問題的,可是在屢次實驗中,一直都是 D C A 的順序,而 UIGestureRecognizer 其餘的 Delegate Method 卻有多是不一樣的順序,這是爲何呢?

咱們來看下 View D 這個方法的調用棧:

能夠看到,UITouchesEvent 遍歷了一個 View 數組,系統經過 Hit-Testing 過程獲得了適合響應觸摸事件的 View D,隨後會根據這個 View 的層級關係獲得一個響應鏈 View 數組 [D_view, C_view, A_view, ..., ZTWindow] 而後遍歷這個數組去依次判斷每一個 View 上的 UIGestureRecognizer 是否要接收觸摸事件,沒有綁定到這個響應鏈 View 數組上的 UIGestureRecognizer 再也不有機會去處理觸摸事件,關於緣由後面會解釋。

接下來調用了方法:

D_view_tapGestureRecognizer_touchesBegan
A_view_tapGestureRecognizer_touchesBegan
C_view_tapGestureRecognizer_touchesBegan

D_view--- gestureRecognizerShouldBegin: D_view_tapGestureRecognizer ---
D_view_tapGestureRecognizer_touchesEndedWithState: 3
D_view--- gestureRecognizerShouldBegin: A_view_tapGestureRecognizer ---
A_view--- gestureRecognizerShouldBegin: A_view_tapGestureRecognizer ---
A_view_tapGestureRecognizer_touchesEndedWithState: 3
D_view--- gestureRecognizerShouldBegin: C_view_tapGestureRecognizer ---
C_view--- gestureRecognizerShouldBegin: C_view_tapGestureRecognizer ---
C_view_tapGestureRecognizer_touchesEndedWithState: 3
複製代碼

View A 中 gestureRecognizerShouldBegin: 方法的調用棧:

因爲咱們的 gestureRecognizer:shouldReceiveTouch: 都返回了 YES,3個 View 上的 UIGestureRecognizer 分別收到了 touchesBegantouchesEnd 等觸摸事件相關的 方法並開始對觸摸手勢進行識別。從調用棧中能夠看出,在 touchesEnd 方法中手勢識別完成以後即將進行狀態轉換以前調用了 gestureRecognizerShouldBegin: 方法判斷是否應該進行手勢識別成功的狀態轉換,因爲咱們的方法都返回了 YES,能夠看到在 touchesEnd 方法完成以後3個 UIGestureRecognizer 都成功識別了手勢而且自身的 state 都變成了 UIGestureRecognizerStateEnded,這些與咱們上一小節的描述是相符的。

須要注意的是,對於 A_view_tapGestureRecognizer 和 C_view_tapGestureRecognizer 來講,除了它們各自的 delegate,最上層的 View D 也收到了他們的 gestureRecognizerShouldBegin: 回調,這是爲何呢?回顧上一小節關於這個方法官方文檔的解釋,UIView 自身也有一個 gestureRecognizerShouldBegin: 方法,當 View 不是 UIGestureRecognizer 的 delegate 時,咱們可使用這個方法來使 UIGestureRecognizer 失效。對於全部綁定到父 View 上的 UIGestureRecognizer,除了它們自己的 delegate 以外,Hit-Testing 返回的 View 也會收到這個方法的調用,關於緣由咱們會在下一節進行解釋。

接下來的 log 是 UIGestureRecognizer 是否能夠同時處理觸摸事件的回調方法,其中的 null 是系統的手勢 UIScreenEdgePanGestureRecognizer,因爲 Demo 使用了 UINavigationController 系統會首先判斷這個手勢法是否能同時響應。能夠看到,因爲這3個 View 上一共存在3個 UIGestureRecognizer,系統一共調用了6次回調方法才能夠肯定它們之間的關係,這和咱們上文對該方法的描述相符。

須要注意的是,UIGestureRecognizer 觸摸事件相關的方法 touchesBegan 等和 gestureRecognizerShouldBegin: 對於 View A C D 來講每次運行順序是不同的(gestureRecognizer:shouldReceiveTouch: 每次都是 D C A),但最終 UIGestureRecognizer 的 Action Method 的順序卻必定是 A C D:

A_singleTapGesture
C_singleTapGesture
D_singleTapGesture
複製代碼

同時,當 shouldRecognizeSimultaneouslyWithGestureRecognizer 都返回 NO 時,View D 上的 UIGestureRecognizer 能夠響應成功。這又是什麼緣由呢?

咱們在上一步 UITouchesEvent 遍歷響應鏈 View 數組的過程當中獲得了一個 UIGestureRecognizer 數組 [D_view_tapGestureRecognizer, C_view_tapGestureRecognizer, A_view_tapGestureRecognizer] 隨後系統遍歷了這個數組來進行處理,這裏猜想它們的 touchesBegan 等方法的順序應該與具體的實現有關(我的猜想可能與 UIGestureEnvironment 裏保存的 UIGestureRecognizer 的數據結構實際上不是數組而是圖有關係),而 Action Method 的順序以及最後確保 View D 上的 UIGestureRecognizer 可以響應成功應該也是目前官方未說明的某種機制。

還有一點須要注意的是,gestureRecognizer:shouldReceiveTouch: 與其餘的方法不屬於相同的調用棧,咱們來看下其餘方法的調用棧:

能夠看到,最早由 UIApplication 經過 sendEvent: 發送了 UIEvent 事件,而後被 UIWindow 轉發給了 UIGestureEnvironment,而 UIGestureEnvironment 經過遍歷一個 UIGestureRecognizer 數組來調起相關的 UIGestureRecognizer 方法。

到此爲止,整個過程仍然有不少疑點,咱們從新進行下梳理。

UIEvent 與 UIGestureEnvironment

實際上,系統最早經過 Hit-Testing 機制來對 UIEvent 進行了包裝,咱們先看下 UIEvent 這個類:

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIEvent : NSObject

@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;

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
#else
- (nullable NSSet <UITouch *> *)allTouches;
#endif
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS(3_2);

@end
複製代碼

能夠看到 UIEvent 全部的屬性都是隻讀以防止被修改,在 View A 的 hitTest:withEvent: 方法中,實際傳遞的是它的子類 UITouchesEvent:

在 Hit-Testing 階段,UIEvent 只包含了一個時間戳信息,咱們在 View A 的 hitTest:withEvent: 方法中打斷點來查看下 UITouchesEvent 的內容:

接下來,咱們繼承 UIWindow 來截獲 sendEvent: 事件,並打斷點來查看此時 UIEvent 的信息,此時 UIEvent 中多了 UITouch:

Printing description of event:
<UITouchesEvent: 0x2819a8120> timestamp: 875742 touches: {(
    <UITouch: 0x11bd35960> phase: Began tap count: 1 force: 0.000 window: <UIWindow: 0x11bd1c610; frame = (0 0; 414 736); gestureRecognizers = <NSArray: 0x2825c94d0>; layer = <UIWindowLayer: 0x282bb5900>> view: <DView: 0x11be2faa0; frame = (0 37; 240 61); autoresize = RM+BM; gestureRecognizers = <NSArray: 0x2825f2a60>; layer = <CALayer: 0x282b894a0>> location in window: {234.66665649414062, 482.66665649414062} previous location in window: {234.66665649414062, 482.66665649414062} location in view: {147.66665649414062, 35.666656494140625} previous location in view: {147.66665649414062, 35.666656494140625}
)}
複製代碼

根據 UIEvent 和 UITouchesEvent 的 API 和以上信息,咱們能夠推斷,系統經過 Hit-Testing 記錄了適合響應觸摸事件的 View 與 Window 等信息,在 Hit-Testing 完成以後,建立了 UITouch 並將其保存在 UIEvent 中進行發送。UIApplication 可以經過 sendEvent: 方法發送事件給正確的 UIWindow 正是因爲在 Hit-Testing 過程當中系統記錄了可以響應觸摸事件的 Window。

而 UITouch 中的 UIGestureRecognizer 數組正是經過前面提到的 gestureRecognizer:shouldReceiveTouch: 來生成的,咱們來看下在 Hit-Testing 完成以後,sendEvent: 調用以前,View D 的 gestureRecognizer:shouldReceiveTouch: 方法中的 UITouch:

此時,Window 和 DView 已經經過 Hit-Testing 找到,可是 _gestureRecognizers 仍然爲空,而在該方法返回 YES 以後,咱們在 View C 的 gestureRecognizer:shouldReceiveTouch: 方法中能夠看到:

此時 D_view_tapGestureRecognizer 已經被添加到了數組中,一樣的,在 View A 的方法中,C_view_tapGestureRecognizer 被添加到了數組中,在最終的 UIEvent 中的 UITouch 裏,3個 UIGestureRecognizer 都被保存了起來,因此 UIApplication 才知道如何向正確的 UIGestureRecognizer 發送觸摸事件。

接下來講下 UIGestureEnvironment,咱們能夠認爲它是管理全部手勢的上下文環境,當調用 addGestureRecognizer: 方法時會將 UIGestureRecognizer 加入到其中。下面是 UIGestureEnvironment 的結構:

@interface UIApplication : UIResponder {
    UIGestureEnvironment * __gestureEnvironment;
    }
@end

@interface UIGestureRecognizer : NSObject {
    UIGestureEnvironment * _gestureEnvironment;
    }
@end

@interface UIGestureEnvironment : NSObject {

	CFRunLoopObserverRef _gestureEnvironmentUpdateObserver;
	NSMutableSet* _gestureRecognizersNeedingUpdate;
	NSMutableSet* _gestureRecognizersNeedingReset;
	NSMutableSet* _gestureRecognizersNeedingRemoval;
	NSMutableArray* _dirtyGestureRecognizers;
	NSMutableArray* _delayedTouches;
	NSMutableArray* _delayedTouchesToSend;
	NSMutableArray* _delayedPresses;
	NSMutableArray* _delayedPressesToSend;
	NSMutableArray* _preUpdateActions;
	bool _dirtyGestureRecognizersUnsorted;
	bool _updateExclusivity;
	UIGestureGraph* _dependencyGraph;
	NSMapTable* _nodesByGestureRecognizer;

}

-(void)addGestureRecognizer:(id)arg1 ;
-(void)removeGestureRecognizer:(id)arg1 ;
-(void)_cancelGestureRecognizers:(id)arg1 ;
-(void)_updateGesturesForEvent:(id)arg1 window:(id)arg2 ;
(省略了不少 API)
-(void)_cancelTouches:(id)arg1 event:(id)arg2 ;
-(void)_cancelPresses:(id)arg1 event:(id)arg2 ;
@end
複製代碼

UIApplication 和 UIGestureRecognizer 中保存了同一個 UIGestureEnvironment 對象,根據上面 UIGestureRecognizer 的 Action Method 的調用棧,咱們能夠看到,UIWindow 經過 sendEvent: 發送事件以後,UIGestureEnvironment 接收了這個事件而且最終經過方法:

-[UIGestureEnvironment _updateForEvent:window:] ()
-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] ()
複製代碼

來對 UIGestureRecognizer 相關方法進行調用:

  1. 對可以處理事件的 UIGestureRecognizer 發送 touchesBegan:withEvent: 等觸摸事件的方法
  2. 經過 gestureRecognizerShouldBegin 方法判斷是否應該進行狀態轉換
  3. 詢問 UIGestureRecognizer 的 delegate 是否應該失效或者是否可以同時處理事件 gestureRecognizer:shouldRequireFailureOfGestureRecognizer: gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
  4. UIGestureRecognizer 識別事件以後最終調用了 Action Method

小結

1. 系統在探測階段結束後建立了 UITouch,並封裝了 UIEvent 將其傳遞。

2. 手勢上下文 UIGestureEnvironment 最早收到 UIEvent,並負責通知給相關的 UIGestureRecognizer。

3. UIGestureEnvironment 根據 UIGestureRecognizer 的 delegate 方法來判斷其是否可以對觸摸事件進行響應。

至此,UIGestureRecognizer 對事件的處理以及它們之間的相互關係告一段落。須要注意的是,建議最好使用官方文檔推薦的方法對 UIGestureRecognizer 進行控制,而不要依賴上文中沒有存在於文檔中的具體實現細節和結論,蘋果沒有對外暴露這些,有可能會在接下來的版本中修改具體實現。

手勢識別器與原生觸摸事件

接下來咱們終於能夠對上一節中 UIResponder 相關的系統原生觸摸事件方法進行探討了,咱們去掉 Hit-Testing 與 UIGestureRecognizer 的 delegate 等相關方法的 log:

A_view_tapGestureRecognizer_touchesBegan
D_view_tapGestureRecognizer_touchesBegan
C_view_tapGestureRecognizer_touchesBegan
D_touchesBegan
C_touchesBegan
A_touchesBegan
A_singleTapGesture
D_touchesCancelled
C_touchesCancelled
A_touchesCancelled
C_singleTapGesture
D_singleTapGesture
複製代碼

官方文檔

按照慣例,咱們先來看下官方文檔是怎麼說的:

There may be times when you want a view to receive a touch before a gesture recognizer. But, before you can alter the delivery path of touches to views, you need to understand the default behavior. In the simple case, when a touch occurs, the touch object is passed from the UIApplication object to the UIWindow object. Then, the window first sends touches to any gesture recognizers attached the view where the touches occurred (or to that view’s superviews), before it passes the touch to the view object itself.

Gesture Recognizers Get the First Opportunity to Recognize a Touch

A window delays the delivery of touch objects to the view so that the gesture recognizer can analyze the touch first. During the delay, if the gesture recognizer recognizes a touch gesture, then the window never delivers the touch object to the view, and also cancels any touch objects it previously sent to the view that were part of that recognized sequence.

文檔實際上說的比較清楚:UIWindow 會先將觸摸事件發送給 Hit-Testing 返回的 View 和它的父 View 上的 UIGestureRecognizer,而後纔會發送給這個 View 自己,若是 UIGestureRecognizer 成功識別了這個手勢,以後 UIWindow 不會再向 View 發送觸摸事件,而且會取消以前發送的觸摸事件。

下面讓咱們回到 Demo 來進行驗證。

Demo 驗證

從 log 上看,現象與官方文檔的說法吻合,咱們用幾個調用棧來對其進行進一步證實:

ZTTapGestureRecognizer 的 touchesBegan:withEvent: 的調用棧:

View D 的 touchesBegan:withEvent: 的調用棧:

View D 的 touchesCancelled:withEvent: 的調用棧:

能夠看到,UIWindow 首先經過 sendEvent: 方法通過 UIGestureEnvironment 發送觸摸事件給了 ZTTapGestureRecognizer,隨後經過 sendTouchesForEvent: 方法發送觸摸事件給 View D 並沿着響應鏈傳遞,而當 A_view_tapGestureRecognizer 第一個成功識別手勢以後,UIGestureEnvironment 發起響應鏈的 cancel 並通過 UIApplication 發送給 View D 並沿着響應鏈取消。

UIGestureRecognizer Properties

UIGestureRecognizer 有一些與響應鏈觸摸事件相關的屬性,這裏簡單說明一下:

// default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to be sent to the view for all touches or presses recognized as part of this gesture immediately before the action method is called.
@property(nonatomic) BOOL cancelsTouchesInView;
複製代碼

這個屬性能夠控制當 UIGestureRecognizer 成功識別手勢以後是否要取消響應鏈對觸摸事件的響應,默認爲 YES,設置爲 NO 以後,即便 UIGestureRecognizer 識別了手勢,UIGestureEnvironment 也不會發起對響應鏈的 cancel。

// default is NO. causes all touch or press events to be delivered to the target view only after this gesture has failed recognition. set to YES to prevent views from processing any touches or presses that may be recognized as part of this gesture
@property(nonatomic) BOOL delaysTouchesBegan;
複製代碼

設置爲 YES 時,這個屬性能夠控制在 UIGestureRecognizer 識別手勢期間截斷事件,識別失敗後響應鏈才能收到觸摸事件。

// default is YES. causes touchesEnded or pressesEnded events to be delivered to the target view only after this gesture has failed recognition. this ensures that a touch or press that is part of the gesture can be cancelled if the gesture is recognized
@property(nonatomic) BOOL delaysTouchesEnded;
複製代碼

默認爲 YES,當手勢識別失敗時,若此時觸摸事件已經結束,會延遲一小段時間(0.15s)再調用響應者的 touchesEnded:withEvent:,若設置成NO,則在手勢識別失敗時會當即通知 UIApplication 發送狀態爲 end 的 觸摸事件給 Hit-Testing 返回的 View 以調用 touchesEnded:withEvent: 結束事件響應。

小結

1. UIGestureRecognizer 首先收到觸摸事件,Hit-Testing 返回的 View 延遲收到,二者的調起方法不一樣。

2. 第一個 UIGestureRecognizer 識別成功後,UIGestureEnvironment 會發起響應鏈的 cancel。

3. 能夠經過設置 UIGestureRecognizer 的 Properties 來控制對響應鏈的影響。

UIControl 特例

咱們如今給 Demo 中的 View D 上加一個 UIButton:

- (void)buttonTapped
{
    NSLog(@"D_buttonTapped");
}

- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    
    if (self)
    {
        ZTTapGestureRecognizer *tapGestureRecognizer = [[ZTTapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTapGesture)];
        tapGestureRecognizer.name = @"D_view_tapGestureRecognizer";
        tapGestureRecognizer.delegate = self;
        [self addGestureRecognizer:tapGestureRecognizer];
        
        FButton *button = [[FButton alloc] initWithFrame:CGRectMake(80, 10, 100, 40)];
        button.backgroundColor = [UIColor blueColor];
        [button addTarget:self action:@selector(buttonTapped) forControlEvents:UIControlEventTouchUpInside];
        [self addSubview:button];
    }
    
    return self;
}
複製代碼

點擊 Button,log 顯示以下:

D_view--- gestureRecognizer shouldReceiveTouch: D_view_tapGestureRecognizer ---
C_view--- gestureRecognizer shouldReceiveTouch: C_view_tapGestureRecognizer ---
A_view--- gestureRecognizer shouldReceiveTouch: A_view_tapGestureRecognizer ---
C_view_tapGestureRecognizer_touchesBegan
A_view_tapGestureRecognizer_touchesBegan
D_view_tapGestureRecognizer_touchesBegan
C_view_tapGestureRecognizer_touchesEndedWithState: 5
A_view_tapGestureRecognizer_touchesEndedWithState: 5
D_view_tapGestureRecognizer_touchesEndedWithState: 5
D_buttonTapped
複製代碼

這與咱們想象的徹底不一樣:

  1. UIGestureRecognizer 沒有響應觸摸事件且除了 shouldReceiveTouch 以外的回調沒有被調用。
  2. 觸摸事件沒有沿着響應鏈進行傳遞。
  3. UIButton 成功的響應了觸摸事件。

這又是什麼緣由致使的呢?

官方文檔

慣例,先看官方文檔:

Interacting with Other User Interface Controls

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.

If you have a custom subclass of one of these controls and you want to change the default action, attach a gesture recognizer directly to the control instead of to the parent view. Then, the gesture recognizer receives the touch event first. As always, be sure to read the iOS Human Interface Guidelines to ensure that your app offers an intuitive user experience, especially when overriding the default behavior of a standard control.

此次文檔解釋清楚了全部的緣由:對於 部分 UIControl 來講(本身實現的不行),爲了防止 UIControl 默認的手勢與其父 View 上的 UIGestureRecognizer 的衝突,UIControl 最後會響應觸摸事件,若是想要 UIGestureRecognizer 處理觸摸事件,則須要將其直接與 UIControl 進行綁定。

緣由清楚了,可是咱們仍是回到 Demo 之中來看看系統具體是怎麼作的。

Demo 驗證

咱們去掉 View A C D 中的 log,實現一個 UIButton 的子類 FButton,對 FButton 添加一個 ZTTapGestureRecognizer,而且實現方法 gestureRecognizerShouldBegin:

@implementation FButton

- (void)singleTapGesture
{
    NSLog(@"F_singleTapGesture");
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if ([super gestureRecognizerShouldBegin:gestureRecognizer])
    {
        NSLog(@"F_Button--- gestureRecognizerShouldBegin: %@ YES ---", gestureRecognizer.name);
    }
    else
    {
        NSLog(@"F_Button--- gestureRecognizerShouldBegin: %@ NO ---", gestureRecognizer.name);
    }
    
    return [super gestureRecognizerShouldBegin:gestureRecognizer];
}


- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];

    if (self)
    {
        ZTTapGestureRecognizer *tapGestureRecognizer = [[ZTTapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTapGesture)];
        tapGestureRecognizer.name = @"F_Button_tapGestureRecognizer";
        tapGestureRecognizer.delegate = self;
        [self addGestureRecognizer:tapGestureRecognizer];
    }

    return self;
}

@end
複製代碼

運行 Demo,點擊 Button,log 以下所示:

D_view_tapGestureRecognizer_touchesBegan
F_Button_tapGestureRecognizer_touchesBegan
C_view_tapGestureRecognizer_touchesBegan
A_view_tapGestureRecognizer_touchesBegan

F_Button--- gestureRecognizerShouldBegin: D_view_tapGestureRecognizer NO ---
D_view_tapGestureRecognizer_touchesEndedWithState: 5
F_Button--- gestureRecognizerShouldBegin: F_Button_tapGestureRecognizer YES ---
F_Button_tapGestureRecognizer_touchesEndedWithState: 3
F_Button--- gestureRecognizerShouldBegin: C_view_tapGestureRecognizer NO ---
C_view_tapGestureRecognizer_touchesEndedWithState: 5
F_Button--- gestureRecognizerShouldBegin: A_view_tapGestureRecognizer NO ---
A_view_tapGestureRecognizer_touchesEndedWithState: 5

F_singleTapGesture
複製代碼

能夠看到 View A C D 和 Button F 上的 ZTTapGestureRecognizer 都收到了 touchesBegan 方法,可是最後只有 F_Button_tapGestureRecognizer 最終成功進行了狀態轉換,其緣由就在於方法 gestureRecognizerShouldBegin:

還記得前兩節咱們對於 gestureRecognizerShouldBegin: 的探討嗎,咱們終於可以進一步對其進行解釋了,這個機會仍是留給官方文檔:

Subclasses may override this method and use it to prevent the recognition of particular gestures. For example, the UISlider class uses this method to prevent swipes parallel to the slider’s travel direction and that start in the thumb.

At the time this method is called, the gesture recognizer is in the UIGestureRecognizerStatePossible state and thinks it has the events needed to move to the UIGestureRecognizerStateBegan state.

The default implementation of this method returns YES.

因此,上文提到的部分 UIControl 重寫了該方法,雖然 UIGestureRecognizer 會首先受到觸摸事件,可是在狀態轉換以前,調用了 Hit-Testing 返回的 View 也就是 UIControl 的 gestureRecognizerShouldBegin: 方法,UIControl 會使父 View 上的 UIGestureRecognizer 失效,而本身的 UIGestureRecognizer 卻不會失效,這就是系統實現這個機制的方法。

還有一個問題,爲何觸摸沒有沿着響應鏈進行傳遞呢?慣例,先看文檔:

Controls communicate directly with their associated target object using action messages. When the user interacts with a control, the control sends an action message to its target object. Action messages are not events, but they may still take advantage of the responder chain. When the target object of a control is nil, UIKit starts from the target object and traverses the responder chain until it finds an object that implements the appropriate action method. For example, the UIKit editing menu uses this behavior to search for responder objects that implement methods with names like cut:, copy:, or paste:.

而後咱們看下 FButton 的響應方法的調用棧(去掉 FButton 上的 UIGestureRecognizer):

這裏文檔沒有說全,實際上,FButton 重寫了 touchesBegan:withEvent: 方法,在收到觸摸事件後將其截斷再也不沿響應鏈進行傳遞;在響應觸摸事件時,FButton 使用 Target-Action 機制經過 sendAction:to:forEvent: 方法通知 UIApplication,UIApplication 在經過 sendAction:to:from:forEvent: 方法向 target 發送 action,而當 target 爲 nil 時就會沿着響應鏈進行尋找,知道找到了實現了相應方法的對象。

小結

1. UIGestureRecognizer 仍然會先於 UIControl 接收到觸摸事件。

2. UIButton 等部分 UIControl 會攔截其父 View 上的 UIGestureRecognizer,但不會攔截本身和子 View 上的 UIGestureRecognizer。

3. UIButton 會截斷響應鏈的事件傳遞,也能夠利用響應鏈來尋找 Action Method。

UITableView 與 UIScrollView

當場景中存在 UITableView 和 UIScrollView 時,又會有不同的狀況,感興趣的讀者能夠試着本身研究一下。

問題解決

如今,咱們回過頭看看最初的問題,解決起來應該就比較簡單了。

首先在咱們對外的 View 上添加一個 UITapGestureRecognizer,經過回調使其能與內部的 UIGestureRecognizer 同時處理觸摸事件,而且與內部雙擊手勢不會衝突:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if (gestureRecognizer == self.tapGestureRecognizer)
    {
        return YES;
    }
    
    return NO;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if (gestureRecognizer == self.tapGestureRecognizer && [otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && ((UITapGestureRecognizer *)otherGestureRecognizer).numberOfTapsRequired == 2)
    {
        return YES;
    }

    return NO;
}
複製代碼

因爲單擊手勢要在雙擊手勢判斷失敗後才能觸發,因此會有必定的延遲,這裏最好的辦法是在內部自定義一個 UIGestureRecognizer 來實現雙擊手勢以縮短等待時間:

#import <UIKit/UIGestureRecognizerSubclass.h>

#define UISHORT_TAP_MAX_DELAY 0.2
@interface UIShortTapGestureRecognizer : UITapGestureRecognizer

@end

@implementation UIShortTapGestureRecognizer

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(UISHORT_TAP_MAX_DELAY * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
    {
        // Enough time has passed and the gesture was not recognized -> It has failed.
        if  (self.state != UIGestureRecognizerStateRecognized)
        {
            self.state = UIGestureRecognizerStateFailed;
        }
    });
}
@end
複製代碼

咱們能夠經過修改 UISHORT_TAP_MAX_DELAY 參數來控制等待的時間。

總結

1. 觸摸事件發生後,IOKit 會經過 mach port 傳遞給 SpringBoad 進程,並最終傳遞給了 UIApplication。

2. UIApplication 經過 Hit-Testing 尋找到了最佳響應者,遍歷獲得全部的 UIGestureRecognizer,而後根據最佳響應者、UIGestureRecognizer、Window 建立 UITouch 並將其保存在 UIEvent 中。

3. UIApplication 將 UIEvent 發送給 UIWindow,UIWindow 首先發送事件給 UIGestureRecognizer,而後發送給最佳響應者,事件沿響應鏈傳遞。

4. UIGestureRecognizer 根據 Delegate 以及最佳響應者來判斷是否可以成功進行狀態轉換並取消響應鏈的觸摸事件。

5. 系統實現的部分 UIControl 會截斷響應鏈,並使父 View 上的 UIGestureRecognizer 失效。

參考資料

Event Handling Guide for iOS

iOS 事件響應鏈中 Hit-Test View 的應用

iOS 觸摸事件全家桶

iOS 點擊事件和手勢衝突

深刻淺出iOS事件機制

相關文章
相關標籤/搜索