05-主题|事件响应者链@iOS-应用场景与进阶实践

本文在 01 总纲02 hitTest03 响应者链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 泳道图:手势、响应者与控件的优先级

flowchart TB subgraph 触摸发生 T1[手指按下] end subgraph 系统 S1[hit-test 得到 view] S2[手势识别器优先] end subgraph 手势层 G1[识别成功?] G2[消费事件] G3[识别失败] end subgraph 响应者层 R1[视图 touches / UIControl] R2[沿 next 传递] end T1 --> S1 S1 --> S2 S2 --> G1 G1 -->|是| G2 G1 -->|否| G3 G3 --> R1 R1 --> R2

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 应用场景分类(思维导图)

mindmap root((应用场景)) 列表与 Cell Cell 内按钮 delegate 扩大热区 小图标 浮层与遮罩 穿透 hitTest 返回 nil 手势与按钮共存 编辑与输入 编辑菜单 copy paste 自定义 inputView 手势与链 手势优先 再响应者链 gestureRecognizerShouldBegin

四、SwiftUI 与 UIKit 的对比(简要)

SwiftUI 没有 UIKit 式的「响应者链」[7][8]

  • 使用 Gesture 修饰符在视图上声明手势,由系统做 hit-test 与手势竞争,不会把事件沿「next」链向上冒泡。
  • 可点击区域由 framecontentShape 决定;.allowsHitTesting(false) 相当于从 hit-test 中排除。
  • 多手势的优先级通过 highPriorityGesturesimultaneousGesture 等显式组合,而非依赖「链」传递。

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")

相关推荐
tangweiguo0305198719 小时前
SwiftUI布局完全指南:从入门到精通
ios·swift
T1an-11 天前
最右IOS岗一面
ios
坏小虎1 天前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年1 天前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技1 天前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
Digitally1 天前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone
Sim14802 天前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
Digitally2 天前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手2 天前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero2 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb