深刻理解 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 是一个系统进程,能够理解为桌面系统,能够统一管理和分发系统接收到的触摸事件。markdown

  1. SpringBoard 进程因接收到触摸事件,触发了主线程 runloop 的 source1 事件源的回调。此时 SpringBoard 会根据当前桌面的状态,判断应该由谁处理这次触摸事件。由于事件发生时,你可能正在桌面上翻页,也可能正在刷微博。如果前者(即前台无 APP 运行),则触发 SpringBoard 自己主线程 runloop 的 source0 事件源的回调,将事件交由桌面系统去消耗;如果后者(即有 APP 正在前台运行),则将触摸事件经过 IPC 传递给前台 APP 进程,接下来的事情即是 APP 内部对于触摸事件的响应了。

APP响应阶段

  1. APP 进程的 mach port 接受到 SpringBoard 进程传递来的触摸事件,主线程的 runloop 被唤醒,触发了 source1 回调。数据结构

  2. source1 回调又触发了一个 source0 回调,将接收到的 IOHIDEvent 对象封装成 UIEvent 对象,此时 APP 将正式开始对于触摸事件的响应。app

  3. source0 回调内部将触摸事件添加到 UIApplication 对象的事件队列中。事件出队后,UIApplication 开始一个寻找最佳响应者的过程,这个过程又称 Hit-Testing,细节将在下一节阐述。另外,此处开始即是与咱们平时开发相关的工做了。异步

  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事件机制

相关文章
相关标签/搜索