前言
在 APP 中,为什么我们点击一个 view, iPhone 就能精准的找到我们点击的那个 view?当我们在复杂的页面中进行交互,系统又是如何将我们的点击进行传递的呢?这一切都是背后的 iOS 系统的响应链在工作。
在 iOS 系统中,只有 UIResponder 及其子类的实例对象来进行事件的接受和处理。常见的 UIResponder 子类有 UIView、UIViewController 和 UIApplication,即 UIKit 中的控件在满足条件的情况下都是可以进行事件的接受和处理。
- 控件不参与响应链的条件:
- isUserInteractionEnabled 为 false
- alpha <= 0.01
- isHidden 为 true
事件派发
当我们手指点击屏幕后,RunLoop 的消息队列会接受到这条消息,下面是该消息的传递路径:
UIKit 会自动帮我们按照这个路径进行事件的传递,来找到最合适的响应者,该响应者也被称为 第一响应者
。第一响应者的确定是跟事件类型相关的,对于触摸事件来说,第一响应者就是发生触摸事件的 view。
既然 UIKit 会自动帮我们寻找第一响应者,那它实际是如何寻找的呢?
如何寻找第一响应者 UIKit 通过 hit-testing 来确定触摸事件发生在哪个视图。它会通过下面步骤来找:
下面几点需要说明一下:
- 视图通过
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 机制来发送通知。当我们与一个带有手势的视图进行交互时,会影响响应链。
- 对一个不带手势的视图滑动
- 对一个带手势的视图滑动
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。