iOS 升级打怪 - Responder Chain

前言

在 APP 中,为什么我们点击一个 view, iPhone 就能精准的找到我们点击的那个 view?当我们在复杂的页面中进行交互,系统又是如何将我们的点击进行传递的呢?这一切都是背后的 iOS 系统的响应链在工作。

在 iOS 系统中,只有 UIResponder 及其子类的实例对象来进行事件的接受和处理。常见的 UIResponder 子类有 UIView、UIViewController 和 UIApplication,即 UIKit 中的控件在满足条件的情况下都是可以进行事件的接受和处理。

  • 控件不参与响应链的条件:
    • isUserInteractionEnabled 为 false
    • alpha <= 0.01
    • isHidden 为 true

事件派发

当我们手指点击屏幕后,RunLoop 的消息队列会接受到这条消息,下面是该消息的传递路径:

截屏2022-01-14 下午11.16.02.png

UIKit 会自动帮我们按照这个路径进行事件的传递,来找到最合适的响应者,该响应者也被称为 第一响应者。第一响应者的确定是跟事件类型相关的,对于触摸事件来说,第一响应者就是发生触摸事件的 view。

既然 UIKit 会自动帮我们寻找第一响应者,那它实际是如何寻找的呢?

如何寻找第一响应者 UIKit 通过 hit-testing 来确定触摸事件发生在哪个视图。它会通过下面步骤来找:

查找第一响应者.png

下面几点需要说明一下:

  • 视图通过 func point(inside point: CGPoint, with event: UIEvent?) -> Bool 来判断视图是否包含事件。
  • 如果事件的位置在视图的 bounds 之外,hitTest(_:with:) 会忽略该视图及其子视图。
  • 如果视图的 clipsToBounds 属性为 false,而它的子视图超出了它的 bounds,即使子视图包含事件的位置,也会忽略子视图。

响应链

响应者需要负责接受原始事件数据,并且它必须做下面的两个事情中的一个:

  • 处理接受的事件。
  • 或者将事件传递给另一个响应者。

若第一响应者不处理这个事件,则会通过 next 传递给响应链中的下一个响应者,若下一个也不处理,则以此类推传递下去。如果到 UIApplication 还没有响应,则会被丢弃。未处理的事件在响应者之间传递,这条传递的路径即响应者链。

响应链本质就是一个链表,表头为第一响应者。它可以通过重写响应者对象的 next 属性来改变。

对于大多数的 UIKit 库的类,系统已经重写了该属性并返回了具体的对象:

  • UIView
    • 是 vc 的根视图,next 为 vc;
    • 不是 vc 的根视图,next 为该视图的父视图。
  • UIViewController
    • 是 window 的根控制器,next 为 window。
    • 被 present 的,next 为 presenting vc。
  • UIWindow,next 为 UIApplication。
  • UIApplication,next 为 app delegate。

UIControl 与 响应链

UIControl 的子类(UIButton/UISlider...)使用 target-action 机制来进行用户交互,当我们点击按钮时, control 会直接发送 action message 给 target 对象。

虽然 action message 不是事件,但它仍可能会使用响应链。比如,当 target 对象为 nil 时。UIApplication 会派发 action message,根据响应链找到合适的响应者去处理。

Gesture 与 响应链

Gesture 也是使用 target-action 机制来发送通知。当我们与一个带有手势的视图进行交互时,会影响响应链。

  • 对一个不带手势的视图滑动

without.png

  • 对一个带手势的视图滑动

截屏2022-01-14 下午11.22.42.png

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let gesture = UIPanGestureRecognizer(target: self, action: #selector(gesturePan(ges:)))
        view.backgroundColor = UIColor.blue
        view.addGestureRecognizer(gesture)
    }
    
    @objc func gesturePan(ges: UIPanGestureRecognizer) {
        print(ges.state)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("touchesBegan")
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("touchesMoved")
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("touchesEnded")
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("touchesCancelled")
    }
}
复制代码

通过打印可以得知:在手势识别成功后,会回调 touchesCancelled 方法,且 touchesMoved 或者 touchesEnded 后续不会再调用。如果想要添加手势的视图还能相应 touches 方法,可以将 cancelsTouchesInView 属性设为 false。

猜你喜欢

转载自juejin.im/post/7053082113356070926