本文在 01 总纲、02 hitTest、03 响应者链、04 UIResponder 基础上,总结工程中的应用场景 与进阶用法:UIControl 的 target=nil 与响应者链、手势识别器与响应者的优先级、扩大点击区域与事件穿透、以及 SwiftUI 与 UIKit 的对比。文末附参考文献。
一、UIControl 与 target=nil 的响应者链
1.1 机制
对 UIControl (如 UIButton、UISlider)使用 addTarget(_:action:for:) 时,若将 target 设为 nil ,系统不会在添加时绑定具体对象,而是在事件触发时 从第一响应者 开始,沿 next 查找第一个能响应该 action 的响应者 并调用,即 action 沿响应者链寻找 target [1]。编辑菜单(复制/粘贴/剪切)也使用同一机制在链上查找实现 copy(_:)、paste(_:)、cut(_:) 等的对象。
1.2 Action 方法签名
Action 方法通常为以下形式之一 [2]:
@IBAction func doSomething()@IBAction func doSomething(sender: UIButton)@IBAction func doSomething(sender: UIButton, forEvent event: UIEvent)
1.3 使用注意
- Cell 内按钮 :按钮在 UITableViewCell/UICollectionViewCell 内时,链的路径是 Cell → 其他 view,不一定会经过 TableView 的 ViewController 。若希望由 VC 处理,用 nil target 可能找不到 VC,此时更稳妥的做法是显式指定 target (如 VC)或通过 delegate/callback 把事件交给 VC [3]。Delegate、Block、闭包、函数封装、遍历传递等「传递方式」的对比与选型见 06-响应者链传递方式与编程模式详解。
- 非相邻 VC:通过 present 的 VC 与当前 VC 不一定在一条「相邻」的 next 链上,nil target 不一定能跨 present 边界找到目标,建议用显式 target 或业务层路由。
二、UIGestureRecognizer 与响应者链
2.1 优先级关系
手势识别器 在触摸到达视图的 touchesBegan 等之前参与识别。若手势识别成功 ,可消费触摸,视图的 touches 方法可能不再被调用;若手势识别失败 ,触摸会交给视图并沿响应者链 继续传递 [4]。
控件(如 UIButton)可通过 gestureRecognizerShouldBegin(_:) 等让父视图的手势不干扰自己的点击,从而保证按钮的 target-action 优先。
2.2 泳道图:手势、响应者与控件的优先级
2.3 小结
| 层级 | 说明 |
|---|---|
| 手势识别 | 先于视图的 touches 参与识别,成功则可消费事件 |
| 视图 touches | 手势未消费时,由 hit-test view 及其 next 链处理 |
| UIControl | 通过内部逻辑与 gesture 的配合,保证点击等行为优先 |
三、扩大点击区域与事件穿透
3.1 扩大点击区域
对视觉较小 的按钮或图标,可通过重写 point(inside:with:) 扩大可点击范围(如四周各扩展 20pt),提升可点性 [5]。
swift
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let margin: CGFloat = 20
return bounds.insetBy(dx: -margin, dy: -margin).contains(point)
}
3.2 事件「穿透」到下层
若希望某视图不响应触摸 、让触摸落到下层视图,可重写 hitTest(_:with:) ,在满足条件时返回 nil ,则当前视图及其子视图不参与命中,系统会继续用其兄弟或父视图参与 hit-test [6]。
swift
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hit = super.hitTest(point, with: event)
// 若希望本视图不拦截,可返回 nil;否则返回 hit
return shouldPassThrough ? nil : hit
}
商用场景示例 :视频播放页上的礼物动画、点赞动效浮层使用 PassThroughView 或重写 hitTest 返回 nil,使点击落到下层进度条、暂停按钮;或活动弹窗关闭后遮罩不拦截,点击空白处关闭。
3.3 手势与按钮共存的完整代码(商用:列表 Cell 内按钮由 VC 处理)
swift
// Cell 内「加购」按钮希望由 ListViewController 处理,用 delegate 传递,避免 nil target 链不到 VC
protocol ProductCellDelegate: AnyObject {
func productCell(_ cell: ProductCell, didTapAddCart productId: String)
}
class ProductCell: UITableViewCell {
weak var delegate: ProductCellDelegate?
private var productId: String = ""
@objc private func addCartTapped() {
delegate?.productCell(self, didTapAddCart: productId)
}
}
class ListViewController: UIViewController, ProductCellDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath) as! ProductCell
cell.delegate = self
cell.configure(productId: items[indexPath.row].id)
return cell
}
func productCell(_ cell: ProductCell, didTapAddCart productId: String) {
// 加购、埋点、弹 toast 等
cartService.add(productId: productId)
}
}
3.4 应用场景分类(思维导图)
四、SwiftUI 与 UIKit 的对比(简要)
SwiftUI 没有 UIKit 式的「响应者链」[7][8]:
- 使用 Gesture 修饰符在视图上声明手势,由系统做 hit-test 与手势竞争,不会把事件沿「next」链向上冒泡。
- 可点击区域由 frame 与 contentShape 决定;.allowsHitTesting(false) 相当于从 hit-test 中排除。
- 多手势的优先级通过 highPriorityGesture 、simultaneousGesture 等显式组合,而非依赖「链」传递。
在 UIKit 与 SwiftUI 混用时,需注意:SwiftUI 宿主视图内的交互由 SwiftUI 管理;嵌入的 UIKit 视图仍走 UIKit 的 hit-test 与响应者链。
五、应用场景小结
| 场景 | 涉及机制 | 建议 |
|---|---|---|
| 按钮/控件由上层 VC 统一处理 | target-action + nil target → 响应者链 | Cell 内或复杂层级下优先显式 target 或 delegate |
| 编辑菜单(复制/粘贴) | 链上查找 canPerformAction / target | 在合适响应者上实现 copy/paste/cut 等方法 |
| 扩大按钮可点区域 | point(inside:with:) | 重写并扩大 bounds 的「有效」区域 |
| 浮层不拦截触摸 | hitTest(_:with:) 返回 nil | 指定条件下返回 nil 实现穿透 |
| 手势与按钮共存 | 手势识别 vs 响应者 | 用 gestureRecognizerShouldBegin 等保护控件 |
| 自定义键盘/输入条 | First Responder + inputView | 成为第一响应者并设置 inputView / inputAccessoryView |
| 事件/回调传递方式选型 | Delegate / Block / 闭包 / 函数封装 / 遍历 | 见 06-响应者链传递方式与编程模式详解 |
5.1 商用场景速查
| 场景 | 做法 |
|---|---|
| 电商列表加购/收藏 | Cell 内小按钮用 delegate 交给 VC,或扩大热区 + target-action |
| 视频/直播浮层不挡点击 | 浮层 view 重写 hitTest 在命中自己时返回 nil |
| 活动弹窗遮罩点击关闭 | 遮罩用 PassThroughView 或 hitTest 返回 nil,按钮在遮罩上方单独处理 |
| 设置页开关/列表点击 | 系统 UITableView 的 didSelect + 响应者链;Cell 内控件用 delegate 更稳 |
| 安全输入/自定义键盘 | 自定义 UITextField 的 inputView,canBecomeFirstResponder = true |
参考文献
1\] [Using responders and the responder chain to handle events - Controls and the responder chain](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fuikit%2Fusing-responders-and-the-responder-chain-to-handle-events "https://developer.apple.com/documentation/uikit/using-responders-and-the-responder-chain-to-handle-events") \[2\] [UIControl \| Apple Developer Documentation](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fuikit%2Fuicontrol "https://developer.apple.com/documentation/uikit/uicontrol") \[3\] [UIControl Target Action event not flowing up the responder chain (Stack Overflow)](https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F19013959%2Fuicontrol-target-action-event-not-flowing-up-the-responder-chain "https://stackoverflow.com/questions/19013959/uicontrol-target-action-event-not-flowing-up-the-responder-chain") \[4\] [Using responders and the responder chain - Gesture recognizers](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fuikit%2Fusing-responders-and-the-responder-chain-to-handle-events "https://developer.apple.com/documentation/uikit/using-responders-and-the-responder-chain-to-handle-events") \[5\] [How to implement point(inside:with:) (Stack Overflow)](https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F53041776%2Fhow-am-i-supposed-to-implement-pointinsidewith "https://stackoverflow.com/questions/53041776/how-am-i-supposed-to-implement-pointinsidewith") \[6\] [Hacking Hit Tests (Khanlou)](https://link.juejin.cn?target=https%3A%2F%2Fkhanlou.com%2F2018%2F09%2Fhacking-hit-tests%2F "https://khanlou.com/2018/09/hacking-hit-tests/") \[7\] [SwiftUI Gesture System Internals (DEV)](https://link.juejin.cn?target=https%3A%2F%2Fdev.to%2Fsebastienlato%2Fswiftui-gesture-system-internals-1j6b "https://dev.to/sebastienlato/swiftui-gesture-system-internals-1j6b") \[8\] [SwiftUI Hit-Testing \& Event Propagation Internals (DEV)](https://link.juejin.cn?target=https%3A%2F%2Fdev.to%2Fsebastienlato%2Fswiftui-hit-testing-event-propagation-internals-2106 "https://dev.to/sebastienlato/swiftui-hit-testing-event-propagation-internals-2106")