iOS 事件响应链

0x0. 简介

iOS 中的事件类型有Touch eventPress eventsShake-motion eventsRemote-control events,本文暂时只分析最常用的Touch event

App 使用 responder 对象 (UIResponder实例) 来接收处理事件。常见的子类包括 UIView, UIViewController 和 UIApplication。当 responder 对象接收到原始事件数据后,必须处理事件或将其转发给另一个 responder 对象。

🌴

0x1. 查找响应者

事件要想被处理,就必须得有个 responder 来响应它。那么一个事件是如何找到它的 responder 的呢?

0x1.1. hitTest:withEvent:

这个方法相信大家一定不陌生,UIKit 便是使用这个方法 (点击测试) 来查找触摸事件的响应者。

该方法内部会调用pointInside:withEvent:方法:

  • 如果pointInside:withEvent:返回 YES,则会遍历当前视图的所有 subview,对每个 subview 依次调用hitTest:withEvent:方法。如果 subview 还有 subview,则以此类推,直到找到包含触摸位置的最前面的视图 (即最后 add 的视图) 为止。
  • 如果pointInside:withEvent:返回 NO,那么hitTest:withEvent:方法会返回 nil,并且当前视图的 subview 也不会被遍历。

subview 的遍历顺序,是从后往前遍历,即先遍历最后 add 的子视图。当然,并不是所有的 view 都会遍历,如果一个 view 满足下列条件任意一个或多个,将会被忽略。
1. hidden = YES
2. userInteraction = NO
3. alpha < 0.01

通过断点调试,推测出伪代码如下:

static BOOL _UIViewIgnoresTouchEvents(UIView *view) {
    if (!view.isUserInteractionEnabled || view.isHidden || view.alpha < 0.01) {
        return YES;
    }
    return NO;
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (_UIViewIgnoresTouchEvents(self)) {
        return nil;
    }
    if ([self pointInside:point withEvent:event] == NO) {
        return nil;
    }
    
    __block UIView *view = nil;
    [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        view = [obj hitTest:point withEvent:event];
        if (view) {
            *stop = YES;
        }
    }];
    return view ? view : self;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

稍微留意下,会发现虽然 touch 了一次,但是hitTest:withEvent:却调用了两次,关于这个问题,只找到了这么一个差不多的解释

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.

🌴

0x2. 处理事件

当找到 responder 对象时,会优先判断是否响应手势。如果 responder 上添加了手势,并且手势代理方法gestureRecognizer:shouldReceiveTouch:返回 YES,那么就会直接响应手势。

如果没有手势可以响应,那么就会调用touchesBegan:withEvent:方法。若想自己处理事件,可以重写该方法,重写后,通常不需要调用 super,只需处理事件即可。如果调用了[super touchesBegan:touches withEvent:event];,则事件将会沿着响应链转发传递。

比如,我们子类化一个 UIButton,重写了touchesBegan:withEvent:且没有调用 super ,那么当点击 button 时,你会发现添加在 button 上的 action 并不会调用,因为事件默认在touchesBegan:withEvent:中处理了。

从官方文档中,取一张事件响应链的图片:


参考链接: https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events

最后更新: 2/1/2021, 5:39:58 PM