温故而知新-iOS事件响应链

1,140 阅读2分钟

简介

用一句话概括响应链过程:先寻找第一响应者,若它无法处理该事件,则传递给下一响应者。

大致的响应过程如图(左边为一个手机界面,右边对应此界面的响应过程)

20210315-104757.png

  1. 当一个触摸事件生成时,系统会将其加入到 UIApplication 管理的事件队列中。
  2. UIApplication 会取出队列最前面事件,分发给合适的 Window。
  3. Window 会在当前视图中,寻找最合适的视图来响应事件,即第一响应者。
  4. 视图主要是借助 hitTest(_:with:) 方法,来寻找最合适的子视图中以响应事件。
  5. 当找到最合适视图后,若该视图无法处理此事件,则传递给下一响应者。

寻找第一响应者过程

可以这样概括:

向每个 subView 询问在不在你的范围内,在的话,就继续往里找,直到没有 subView 时,返回自己。

核心就是 hitTest(_:with:) 方法,大致逻辑如下:

extension UIView {
    func myHitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard self.isHidden == false, self.alpha > 0, // 没隐藏
                self.isUserInteractionEnabled == true, // 可交互
                self.point(inside: point, with: event) == true /* 在范围内 */ else {
            return nil
        }

        for v in self.subviews.reversed() { // 从最上层的 view 开始寻找最佳响应者
            let p = self.convert(point, to: v)
            if let firstResponder = v.myHitTest(p, with: event) {
                return firstResponder
            }
        }

        // 无 subView 或 找不到能响应的 subView,则返回自己
        return self
    }
}

事件传递给下一响应者

当找到最合适视图后,若该视图无法处理该事件,则传递给下一响应者。

比如,label.isUserInteractionEnabled = true,但是没有添加手势,此时即使点击 label,它也无法响应,会将事件传给 nextResponder。

应用

如何响应 view 之外的事件?

覆写 pointInside:withEvent: 方法,在寻找事件接收者时,扩大其响应区域。

实际开发中,可能会借助关联对象和 Category 配合实现。

那可不可以通过重写 hitTest 方法来实现呢? 非要这样做也可以,只是要将 -[UIView pointInside:withEvent:] 的逻辑在 hitTest 中完成。 但这样破坏了原有链路,而且远没有直接在 pointInside 里处理方便。

其他

关于寻找第一响应者的解释

官方文档

Determining Which Responder Contained a Touch Event UIKit uses view-based hit-testing to determine where touch events occur. Specifically, UIKit compares the touch location to the bounds of view objects in the view hierarchy. The hitTest(_:with:) method of UIView traverses the view hierarchy, looking for the deepest subview that contains the specified touch, which becomes the first responder for the touch event.

hitTest 会被调用多次

笔者测试时,发现即使是单次点击,hitTest 也会被调用多次。 google 后发现这是正常的:Apple - Lists.apple.com

感谢

iOS 中事件的响应链和传递链 - 掘金 Using Responders and the Responder Chain to Handle Events | Apple Developer Documentation 参考 最好的输入方式:iOS 中的触摸事件 - 简书