iOS 的响应链小结

2,458 阅读20分钟

iOS 的响应链

1. iOS的中事件的产生

说起到事件,就很容易让人想到Runloop,因为Runloop就是iOS中的事件处理框架。而我们在开发中常见的事件的产生也是由Runloop去消费的。

iOS中的事件分成好几种:

  1. 触摸事件,手指触摸屏幕时产生
  2. 加速事件,手机的陀螺仪以及加速度计产生的
  3. 远程控制事件,使用其他远程控制设备控制,例如蓝牙设备

iOS触摸事件全家桶 中绘制了如下流程,事件完整的生命周期如下:

  1. 手指触摸屏幕,通过IOKit.framework将事件封装成 IOHIDEvent 对象
  2. 系统寻找前台APP,APP在Runloop中注册mach port的Source1监听事件
  3. Runloop中指定mach port监听Source1事件的回调方法被触发__IOHIDEventSystemClientQueueCallback(). 在回调方法中, IOHIDEvent对象就被添加到Application的时间处理队列EventQueue中
  4. 当EventQueue中存在待处理事件时, 就会触发Source0, Source0的回调方法 __processEventQueue()就是从EventQueue中取出事件包装成UIEvent去分发事件, 找到合适的相应对象!!!
  5. 寻找合适的事件相应对象, 具体方式就是调用UIWindow的hitTest:withEvent:方法进行hit-testing
  6. 在找到hit-tested view以后, UIEvent就会在响应者链(Responder-Chain)中传递, 最终被响应者消费,或者丢弃
  7. 另外UIEvent不仅可以被UIResponder消费, 还能被UIGestureRecognizer或是target-action模式捕获, 这里涉及到三者的优先级

第5步中: UIWindow的hitTest:withEvent: 方法执行过程中UIEvent中的几乎没有关键内容

当 hitTest 走完全过程, 找到最佳响应者时, UIEvent对象中的内容就非常丰富了

UIEvent事件在iOS APP中完整的生命周期如下图:

img

UIEvent事件的整个生命周期中有以下重难点:

  1. UITouch, UIEvent, UIResponder的关系
  2. hit-testing的流程
  3. 事件的响应以及事件在响应者链中的传递
  4. UIResponder与UIGestureRecorgnizer, UIControl处理事件的优先级

1. UITouch, UIEvent, UIResponder

UITouch:

顾名思义, 手指触摸屏幕会产生UITouch对象! 触摸对象会记录一些关键信息, 包括以下内容:

@interface UITouch : NSObject

@property(nonatomic,readonly) NSTimeInterval      timestamp; // 触摸发起的时间
@property(nonatomic,readonly) UITouchPhase        phase; // 触摸的各个阶段状态
@property(nonatomic,readonly) NSUInteger          tapCount; // 快速双击
@property(nonatomic,readonly) UITouchType         type API_AVAILABLE(ios(9.0)); //手指/pencil

@property(nullable,nonatomic,readonly,strong) UIWindow                        *window;
@property(nullable,nonatomic,readonly,strong) UIView                          *view;
@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizers 
...
@end

// 触摸的各个阶段状态, 
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)
    UITouchPhaseRegionEntered   API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos),  // whenever a touch is entering the region of a user interface
    UITouchPhaseRegionMoved     API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos),  // when a touch is inside the region of a user interface, but hasn’t yet made contact or left the region
    UITouchPhaseRegionExited    API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos),  // when a touch is exiting the region of a user interface
};

UIEvent:

触摸的目标是生成一个事件UIEvent, 这个事件交给 UIResponder 消费

另外, UIEvent 也可以被 UIGestureRecorgnizer或者UIControl消费.

UIResponder:

每个响应者都是一个UIResponder对象, 常见的 UIView, UIViewController, UIApplication, UIAppDelegate都都UIResponder的子类. 他们能响应UIEvent, 因为他们会实现 UIResponder的4个关键方法:

// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

这个几个方法描述了UIResponder对象收到UITouchEvent时, 对事件做出响应!!!

另外也可以调用super相关方法, 将事件通过Responder chain 传递事件!!!

2. Hit-Testing 的过程

当APP收到触摸事件以后, 会被放入Application的事件分发队列中, 然后通过Hit-Testing找到一个最佳UIResponder来响应这个事件. (最佳响应者就是First Responder, 而这个UIView被称为hit-tested View)

这个查找过程是下而上查找的(UIWindow在最底层, 从底层view向上层找view), 一句话描述:

递归询问SubView是否能响应事件的过程, 并且如果有多个SubViews, 从SubViews.lastObject开始, 从前往后判断.

判断的过程关键节点是两个方法, UIView的两个方法:

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

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

具体实现如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
  	 // 不允许交互, 隐藏, 透明 View 不能响应事件
     if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01){
         return nil; 
     }
    // 触摸点若不在当前View上, 则无法响应事件
    // 这个方法判断触摸点是否在当前View中
    if ([self pointInside:point withEvent:event] == NO) {
         return nil; 
     }
     
    // 从后往前遍历子视图数组! 进行 Hit-Testing 判断
    int count = (int)self.subviews.count; 
    for (int i = count - 1; i >= 0; i--)   { 
        UIView *childView = self.subviews[i];
        // 坐标系的转换i, 把触摸点在当前视图上坐标转换为在子视图上的坐标
        CGPoint childP = [self convertPoint:point toView:childView]; 
        // 询问SubView 中是否存在最佳响应视图
        UIView *fitView = [childView hitTest:childP withEvent:event]; 
        if (fitView) {
            //如果SubView中存在最佳View,返回它
            return fitView; 
        }
    } 
  
    //没有在子视图中找到更合适的响应视图,那么自身就是最合适的
    return self;
}

有一个实例, 如果有以下实例, 点击View E:

A
├── B
│   └── D
└── C
    ├── E
    └── F

那么hit-testing的顺序是:

A->C->F->E

如果我们自己实现UIView的-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法,在其中调用super方法就会去subview中递归进行hit-test!

在这里, 如果有一些特殊的交互需求, 例如在Tabbar中凸圆起按钮需要点击, 可以实现TabbarhitTest:WithEvent方法, 在重写pointInside:withEvent时, 将中心凸起圆Rect内容, 判断成Tabbar内部!!! 此时事件就能继续朝SubViews进行hit-test判断了!!

//TabBar 重写 PointInside!
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    //将触摸点坐标转换到在CircleButton上的坐标
    CGPoint pointTemp = [self convertPoint:point toView:_CircleButton];
    //若触摸点在CricleButton上则返回YES
    if ([_CircleButton pointInside:pointTemp withEvent:event]) {
        return YES;
    }
    //否则返回默认的操作
    return [super pointInside:point withEvent:event];
}

作者:无声编码器
链接:https://juejin.cn/post/6954549838474117127
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

UIEvent的响应与UIResponder Chain

前面通过Hit-Test方式找到了Hit-Tested-View. 系统会调用UIApplication的sentEvent:方法,通过UIWindow的同名方法调用hit-tested-View的相关方法:

UIApplication -> UIWindow -> hit-tested View

这里有一个问题, UIApplication是如何确认UIWindow的呢!!! -- 因为在hit-testing过程中, UIEvent中会记录整个过程中出现的关键内容!!!(实际上UIEvent中的UITouch会绑定触摸时, 保存的UIWindow和UIView)

另外如果要响应UIEvent, 必须是一个UIResponder的对象,该对象可以实现以下几个方法就可以处理触摸事件了:

- (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];
}

如果Hit-Tested View被称为最佳响应者, 拥有最优先相应权力, 它可以选择响应这个事件, 或者将这个事件沿着响应链向下传递!!! 这个过程是自上而下的。前面找hit-testing过程称为寻找,这里响应事件的过程称为响应.

UIResponder响应者接受到事件的处理都是需要调用touchesBegin:withEvent方法, 具体对事件的响应可有如下操作:

  1. 默认实现,是调用super touchesBegin:withEvent:, 这样该事件会沿着响应链向下传递
  2. 拦截事件, 独吞事件. 重写该方法, 不调用 super touchesBegin:withEvent: 方法
  3. 拦截事件, 继续分发. 重写方法, 处理事件, 同时调用 super touchesBegin:withEvent:方法

UIResponder拥有一个nextResponder属性, 可以获取下一个响应者, 在Hit-Testing过程中, 这个响应者链就固定了!!!

UIResponder Chain的流程如下:

  1. UIView: 如果是root view, 那么nextResponder就是它的ViewController, 否则是father View
  2. UIViewController:
    1. 如果是Window的RootViewController, 那么nextResponder是UIWindow
    2. 如果UIViewController是被其他VC present出来的, 那么nextResponder是 presented VC
    3. 如果UIViewController是其他VC的childVC, 例如UINavigationController, 那么nextResponder 是father VC中的一些View
  3. UIWindow: nextResponder是UIApplication对象
  4. UIApplication, 可能是 APP Delegate对象

一个典型的View的 Responder Chains:

CircleButton --> CustomeTabBar --> UIView --> UIViewController --> UIViewControllerWrapperView --> UINavigationTransitionView --> UILayoutContainerView --> UINavigationController --> UIWindow --> UIApplication --> AppDelegate


作者:无声编码器
链接:https://juejin.cn/post/6954549838474117127
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

UIGestureRecognizer与 UIResponder的联系

手势识别器(UIGestureRecognizer)也是能够响应触摸事件的.

而iOS中的手势, 可以分成两类 :

  1. 离散型手势, 例如UITapGestureRecognizer, UISwipeGestureRecognizer, 识别成功与失败的状态变化如下:
    1. 识别成功: Possible -> Recognized
    2. 识别失败: Possible -> Failed
  2. 持续型手势, 例如UILongPressGestureRecorgnizer以及其他!!! 它们识别成功与失败的状态变化如下:
    1. 识别成功: Possible -> Begin -> [Changed] -> Ended
    2. 不完整识别: Possible -> Begin -> [Changed] -> Cancel

UIResponder与UIGestureRecognizer的优先级

UIGestureRecognizer 也拥有UIResponder相同的4个touchesXXXX方法:

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

离散型手势 TapGesture

在自定义YellowView实现touchesBegin:, 并在father View中增加一个 TapGestureRecognizer, 然后Tap YellowView, 会有如下日志:

//father View的 Gesture 首先收到 Touch Begin, 然后才是 subView 收到 touch begin
2021-09-07 12:29:46.232375+0800 TouchEventLib-master[62247:2632529] -[GLWindow hitTest:withEvent:]
2021-09-07 12:29:46.232951+0800 TouchEventLib-master[62247:2632529] -[GLWindow hitTest:withEvent:]
2021-09-07 12:29:46.233875+0800 TouchEventLib-master[62247:2632529] -[GLApplication sendEvent:]
2021-09-07 12:29:46.234152+0800 TouchEventLib-master[62247:2632529] -[LXFTapGestureRecognizer touchesBegan:withEvent:]
2021-09-07 12:29:46.234515+0800 TouchEventLib-master[62247:2632529] -[YellowView touchesBegan:withEvent:]
2021-09-07 12:29:46.298401+0800 TouchEventLib-master[62247:2632529] -[GLApplication sendEvent:]
2021-09-07 12:29:46.298667+0800 TouchEventLib-master[62247:2632529] -[LXFTapGestureRecognizer touchesEnded:withEvent:]
2021-09-07 12:29:46.298903+0800 TouchEventLib-master[62247:2632529] View Taped
2021-09-07 12:29:46.299070+0800 TouchEventLib-master[62247:2632529] -[YellowView touchesCancelled:withEvent:]

在自定义YellowView实现touchesBegin:, 并在自己身上同同中增加一个 TapGestureRecognizer, 然后Tap YellowView, 会有如下日志:

2021-09-07 12:31:50.317633+0800 TouchEventLib-master[63563:2642415] -[GLWindow hitTest:withEvent:]
2021-09-07 12:31:50.318237+0800 TouchEventLib-master[63563:2642415] -[GLWindow hitTest:withEvent:]
2021-09-07 12:31:50.319273+0800 TouchEventLib-master[63563:2642415] -[GLApplication sendEvent:]
2021-09-07 12:31:50.319619+0800 TouchEventLib-master[63563:2642415] -[LXFTapGestureRecognizer touchesBegan:withEvent:]
2021-09-07 12:31:50.319939+0800 TouchEventLib-master[63563:2642415] -[YellowView touchesBegan:withEvent:]
2021-09-07 12:31:50.384460+0800 TouchEventLib-master[63563:2642415] -[GLApplication sendEvent:]
2021-09-07 12:31:50.384791+0800 TouchEventLib-master[63563:2642415] -[LXFTapGestureRecognizer touchesEnded:withEvent:]
2021-09-07 12:31:50.385104+0800 TouchEventLib-master[63563:2642415] View Taped
2021-09-07 12:31:50.385339+0800 TouchEventLib-master[63563:2642415] -[YellowView touchesCancelled:withEvent:]

官方文档中解释到:

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.

小结:

  1. 当前UIView中, 其父view (或者自己的)的 UIGestureRecognizer 比自己的 UIResponder具有更高的事件响应优先级!!!
  2. UIWindow 先将事件传递给这些手势识别器, 然后再传给hit-tested view
  3. 一旦有手势识别器成功识别了手势,UIApplication就会取消hit-tested view对事件的响应调用cancel

持续型手势 PanGesture

在自定义YellowView实现touchesBegin:, 并在father View中增加一个 PanGestureRecognizer, 然后Pan YellowView, 会有如下日志:

2021-09-07 12:42:18.759136+0800 TouchEventLib-master[67569:2666886] -[GLWindow hitTest:withEvent:]
2021-09-07 12:42:18.759402+0800 TouchEventLib-master[67569:2666886] -[GLWindow hitTest:withEvent:]
2021-09-07 12:42:18.759896+0800 TouchEventLib-master[67569:2666886] -[GLApplication sendEvent:]
2021-09-07 12:42:18.760113+0800 TouchEventLib-master[67569:2666886] -[LXPanGestureRecognizer touchesBegan:withEvent:]
2021-09-07 12:42:18.760293+0800 TouchEventLib-master[67569:2666886] -[YellowView touchesBegan:withEvent:]
2021-09-07 12:42:18.781836+0800 TouchEventLib-master[67569:2666886] -[GLApplication sendEvent:]
2021-09-07 12:42:18.782021+0800 TouchEventLib-master[67569:2666886] -[LXPanGestureRecognizer touchesMoved:withEvent:]
2021-09-07 12:42:18.782159+0800 TouchEventLib-master[67569:2666886] -[YellowView touchesMoved:withEvent:]
2021-09-07 12:42:18.805533+0800 TouchEventLib-master[67569:2666886] -[GLApplication sendEvent:]
2021-09-07 12:42:18.805856+0800 TouchEventLib-master[67569:2666886] -[LXPanGestureRecognizer touchesMoved:withEvent:]
2021-09-07 12:42:18.806108+0800 TouchEventLib-master[67569:2666886] -[YellowView touchesMoved:withEvent:]
2021-09-07 12:42:18.840644+0800 TouchEventLib-master[67569:2666886] -[GLApplication sendEvent:]
2021-09-07 12:42:18.840874+0800 TouchEventLib-master[67569:2666886] -[LXPanGestureRecognizer touchesMoved:withEvent:]
2021-09-07 12:42:18.841192+0800 TouchEventLib-master[67569:2666886] View panned
2021-09-07 12:42:18.841367+0800 TouchEventLib-master[67569:2666886] -[YellowView touchesCancelled:withEvent:]
2021-09-07 12:42:18.861238+0800 TouchEventLib-master[67569:2666886] -[GLApplication sendEvent:]
2021-09-07 12:42:18.861484+0800 TouchEventLib-master[67569:2666886] -[LXPanGestureRecognizer touchesMoved:withEvent:]
2021-09-07 12:42:18.861646+0800 TouchEventLib-master[67569:2666886] View panned

滑动手指时, 结论如下:

  1. 开始滑动手指, Recognizer处于识别state, 此时滑动产生的事件, 会UIApplicaiton会发送给Recognizer, 然后发给 YelloView.

  2. 在一段时间以后, Recognizer识别到手势, 此时action调用, 同时, UIApplication会发送cancel给YelloView的事件响应. 只有Recognizer响应事件

  3. 另外, 在滑动的过程中,如果手势一直没有被识别, 则事件会一直传递给hit-tested view,直到触摸结束

Recorgnizer 与 UIResponder 的阶段小结

当触摸发生或者触摸的状态发生变化时,Window都会传递事件寻求响应

  1. UIWindow先将绑定UITouch的UIEvent传递给UITouch中绑定的手势识别器, 再发送给UITouch的hit-tested view
  2. Recognizer识别手势期间,若UITouch的触摸State发生变化, 事件会被UIApplication优先发送给Recognizer, 然后再发送给hit-test view
  3. 如果Recognizer成功识别了手势(state 变化), 则通知Application调用 hit-tested view的cancelTouch方法, 并在后续停止向hit-tested view发送事件
  4. 如果Recognizer未能识别手势, 而此时触摸并未结束, 则停止向手势识别器发送事件, 仅向hit-test view发送事件.
  5. 若手势识别器未能识别手势, 且此时触摸已经结束, 则向hit-tested view发送end状态的touch事件以停止对事件的响应.

作者:Lotheve 链接:juejin.cn/post/684490… 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

手势Recorgnizer的关键属性

UIRecorgnizer都会有以下三个关键属性可以设置:

// a UIGestureRecognizer receives touches hit-tested to its view and any of that view's subviews
@property(nullable, nonatomic,readonly) UIView *view;           // the view the gesture is attached to. set by adding the recognizer to a UIView using the addGestureRecognizer: method

@property(nonatomic) BOOL cancelsTouchesInView;       // 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 delaysTouchesBegan;         // 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 delaysTouchesEnded;         // 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

这三个属性都是 Recorgnizer 处理事件时, 对 UIResponder的影响配置. 可以参考注释.

UIControl 与 UIResponder的联系

UIControl是系统提供的另外一种Target:Action:事件处理的控件, 它继承自UIView (因此也会是继承自UIResponder), iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子类。 当UIControl跟踪到触摸事件时, 会向其上添加的target发送事件来执行action.

这里需要理解这两个内容:

  1. target-action执行时机及过程
  2. 触摸事件优先级

Target-Action机制

UIControl是能够响应事件的控件, 因此必然也需要待事件交互符合条件时才去响应, 因此也会跟踪事件发生的过程。不同于UIControl以及UIGestureRecognizer通过 touches 系列方法跟踪,UIControl有其独特的跟踪方式:

// Called when a touch event enters the control’s bounds.
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
// Called when a touch event for the control updates.
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
// Called when a touch event associated with the control ends.
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event; // touch is sometimes nil if cancelTracking calls through to this.
// Tells the control to cancel tracking related to the specified event.
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;   // event may be nil if cancelled for non-event reasons, e.g. removed from window

这4个方法与前面UIResponder的touchesxxx系列的方法非常像!!!

因为系统的UIControl中的Responder相关的方法都进行了重写.

UIControl的 Tracking 系列方法是在 touch 系列方法内部调用的, 例如beginTrackingWithTouch 是在 touchesBegan 方法内部调用的.

当UIControl Tracking到指定的事件以后, 会触发 Target:Action:的响应, 流程如下:

  1. UIControl通过addTarget:action:forControlEvents:缓存了 target, action
  2. 然后在事件触发时, 会调用sendAction:to:for:Event:通知UIApplication
  3. UIApplication调用sendAction:to:from:forEvent:方法触发 target的action

因此, 如果要做全埋点, 可以HOOK UIApplication的sendAction:to:from:forEvent:方法来监听UIControl的点击事件!!!

UIControl 与 GestureRecognizer的优先级

官方文档中解释到:

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)来说, 为了防止 UIControl 默认的Target-Action与其父 View 上的 UIGestureRecognizer 的冲突, 系统会默认设定UIControl 来响应触摸事件

小结:

  1. UIControl会阻止father View上的GestureRecognizer!!!!
  2. UIControl自己身上的GestureRecognizer比UIControl的Target-Action优先级更高!!!

juejin.cn/post/684490… 中有两个Demo:

// 预置场景:父View BlueView上添加一个button,同时给button添加一个target-action事件。

//示例一 -- 在父View上添加 GestureRecognizer
-[CLTapGestureRecognizer touchesBegan:withEvent:]
-[CLButton touchesBegan:withEvent:]
-[CLButton beginTrackingWithTouch:withEvent:]
-[CLTapGestureRecognizer touchesEnded:withEvent:] after called state = 5 //UIGestureRecognizerStateFailed 手势识别失败
-[CLButton touchesEnded:withEvent:]
-[CLButton endTrackingWithTouch:withEvent:]
按钮点击

//示例二 -- 在子View Button上添加 GestureRecognizer
-[CLTapGestureRecognizer touchesBegan:withEvent:]
-[CLButton touchesBegan:withEvent:]
-[CLButton beginTrackingWithTouch:withEvent:]
-[CLTapGestureRecognizer touchesEnded:withEvent:] after called state = 3  //UIGestureRecognizerStateEnded 手势识别完成
手势触发
-[CLButton touchesCancelled:withEvent:]
-[CLButton cancelTrackingWithEvent:]

上面现象中:

  1. 重点(不论Gesture添加到Father还是自己身上): 点击button以后, UIEvent 仍然优先被发送给Recognizer, 然后传递给hit-tested View!!!
  2. UIControl的 touchesBegan 会调用beginTrackingWithTouch方法!!!
  3. 在示例一中, UIControl阻止了Father View的GestureRecognizer!!! 导致 Recognizer 识别失败!!! 此时 state = 5!!! 也就是UIGestureRecognizerStateFailed.
  4. UIControl阻止Gesture以后, 后续事件在touchesEnded中调用endTrackingWithTouch方法,并响应
  5. 在示例二中, 由于UIControl无法阻止在自己身上的GestureRecognizer, 因此最终是手势识别, 而UIResponder的touchesCancel被调用

UIControl阻止 father view 的UIGestureRecognizer原因

首先, UIView也有一个gestureRecognizerShouldBegin方法:

@interface UIView (UIViewGestureRecognizers)

@property(nullable, nonatomic,copy) NSArray<__kindof UIGestureRecognizer *> *gestureRecognizers API_AVAILABLE(ios(3.2));

- (void)addGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer API_AVAILABLE(ios(3.2));
- (void)removeGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer API_AVAILABLE(ios(3.2));

// called when the recognizer attempts to transition out of UIGestureRecognizerStatePossible if a touch hit-tested to this view will be cancelled as a result of gesture recognition
// returns YES by default. return NO to cause the gesture recognizer to transition to UIGestureRecognizerStateFailed
// subclasses may override to prevent recognition of particular gestures. for example, UISlider prevents swipes parallel to the slider that start in the thumb
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer API_AVAILABLE(ios(6.0));

@end

gestureRecognizerShouldBegin:方法的简单解释:

  1. 当GestureRecognizer的state状态从UIGestureRecognizerStatePossible转换出时, hit-tested 测试到这个view时可能会调用这个方法
  2. 默认情况下返回YES. 如果return NO, 导致手势识别器转换为UIGestureRecognizerStateFailed
  3. 子类可以重写以这个方法阻止一些特殊的手势!!! 例如, UISlider防止平行于从拇指开始的滑块滑动.

UIControl 内部重写了 UIView 提供的的gestureRecognizerShouldBegin方法返回 false,使父 View 上的手势不参与到事件响应中去,但是不会影响其自身的手势

UIGestureRecognizer可以设置一个delegate -- UIGestureRecognizerDelegate, 它拥有控制手势识别器识别状态相关的方法

特殊情况, ScrollView/TableView中的特殊手势!!!

在ScrollView和TableView中, 可能会被系统添加的特殊手势!!! -- UIScrollViewDelayedTouchesBeganGestureRecognizer

当用户在 UIScrollView 的一个子视图上按下时,UIScrollView并不知道用户是想要滑动内容视图还是点击对应子视图,所以在按下的一瞬间, 事件 UIEventUIApplication 传递到 UIScrollView 后,其会先将该事件拦截而不会立即传递给对应的子视图, 同时开始一个 150ms 的倒计时,并监听用户接下来的行为。

1. 当倒计时结束前,如果用户的手指发生了移动,直接滚动内容视图,不会将该事件传递给对应的子视图;
2. 当倒计时结束时,如果用户的手指位置没有改变,则调用自身的 -touchesShouldBegin:withEvent:inContentView:方法询问是否将事件传递给对应的子视图 (如果返回 NO, 则该事件不会传递给对应的子视图,如果返回 YES,则该事件会传递给对应的子视图,默认为 YES);
3. 当事件被传递给子视图后, 如果手指位置又发生了移动, 则调用自身的 -touchesShouldCancelInContentView: 方法询问是否取消已经传递给子视图的事件。

作者:CoderStar
链接:https://juejin.cn/post/6989049513172303885
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

常规问题: UICollectionView 父 view 添加手势,其内部代理 didSelectItemAt 不触发

tapViewGesture.delegate = self

override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
  let p = gestureRecognizer.location(in: superview)
  let v = superview.hitTest(p, with: nil)
  /// gestureRecognizer.view为父View。
  /// hitTest返回为父View,则返回true,手势生效;
  /// 如果返回为UICollectionView,则返回false,手势不生效,UICollectionView的didSelectItemAt可以正常触发。
  return v == gestureRecognizer.view
}

作者:CoderStar
链接:https://juejin.cn/post/6989049513172303885
来源:掘金
著作权归作者所有商业转载请联系作者获得授权,非商业转载请注明出处

参考

smnh.me/hit-testing…

juejin.cn/post/684490…

www.jianshu.com/p/c294d1bd9…

juejin.cn/post/698904…