iOS 基础 - 处理屏幕点击事件

一些 iOS 基础知识,业务开发中经常用到,面试时也常会被问到,这里总结一下,此篇文章讲解处理屏幕点击事件。
概述
点击事件的处理分成两个部分,第一部分是找到包含点击事件的 Responder,第二部分是决定处理点击事件的第一个 Responder。能够处理点击事件的 Responder 都是 UIResponder 的实例。
找到包含点击事件的 Responder
UIKit 通过调用 UIView 的 hitTest(_:with:) 方法来找到包含 point 的最远子孙视图(包含自身),下面是脑补的系统实现逻辑:
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.isHidden == true
|| self.isUserInteractionEnabled == false
|| self.alpha < 0.01 {
return nil
}
// 保证自身包含 point
guard self.point(inside: point, with: event) else {
return nil
}
// 从叠层角度看,从上到下调用子视图的 hitTest(_:with:),
// 找到第一个非空子孙视图就返回,然后停止
for subview in self.subviews.reversed() {
if let hitView = subview.hitTest(point, with: event) {
return hitView
}
}
return nil
}
下面给出 point(inside:with:) 的定义:
func point(inside point: CGPoint, with event: UIEvent?) -> Bool
所以如果要改变找到包含点击事件的 Responder 的流程,可按需重写 hitTest(_:with:) 或 point(inside:with:)。
示例 1:扩大按钮的点击区域
class SearchHistoryExpandButton: UIButton {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let newArea = CGRect(x: bounds.origin.x - 5,
y: bounds.origin.y - 10,
width: bounds.size.width + 10,
height: bounds.size.height + 20)
return newArea.contains(point)
}
}
示例 2:点击区域限制在四个角
class PhotoEditorResizeView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if bounds.contains(point) {
for area in caculateThumbAreas().values { // 四个角的 rect
if area.contains(point) {
return true
}
}
}
return false
}
}
决定处理点击事件的第一个 Responder
决定处理点击事件的第一个 Responder 的流程会顺着上图的实线走,如果 Text Field 不处理这个事件,UIKit 再发给 Text Field 的父 UIView,再给父父 UIView,然后是父父 UIView 的 UIViewController,此 UIViewController 是 UIWindow 的 rootViewController,所以下一步就是此 UIWindow,再下一步就是 UIApplication,如果实现 UIApplicationDelegate 的对象,也是 UIResponder 的实例,那么就会再传递给此 Delegate。
UIButton 即便没有添加 addTarget,也属于处理了点击事件的 Responder:
button.addTarget(self, action: #selector(onTap), for: .touchUpInside)
一个子 UIView 添加了 UITapGestureRecognizer,就算是处理了点击事件的 Responder,不会再传递给父 UIView:
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))