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 式的「响应者链」78

  • 使用 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

2 UIControl | Apple Developer Documentation

3 UIControl Target Action event not flowing up the responder chain (Stack Overflow)

4 Using responders and the responder chain - Gesture recognizers

5 How to implement point(inside:with:) (Stack Overflow)

6 Hacking Hit Tests (Khanlou)

7 SwiftUI Gesture System Internals (DEV)

8 SwiftUI Hit-Testing & Event Propagation Internals (DEV)

相关推荐
鹤卿1238 小时前
(OC)UI学习——网易云仿写
ui·ios·objective-c
不自律的笨鸟9 小时前
最新屏蔽 iOS 系统更新描述文件保姆级教程
ios
开心猴爷10 小时前
Flutter 如何自动上传 可以 IPA 把构建和上传分开处理
后端·ios
秋雨梧桐叶落莳14 小时前
iOS——QQ音乐仿写项目总结
学习·macos·ui·ios·mvc·objective-c·xcode
iUNPo15 小时前
WWDC26 技术解读:Apple Intelligence、Siri AI 与苹果生态的下一步
macos·ios·wwdc
代码的小搬运工16 小时前
【iOS】谓词与正则表达式
ios
恋猫de小郭16 小时前
解析华为 DevEco Code 和小米 MiMo Code,都基于 OpenCode ,有什么区别?
android·前端·ios
wjm0410061 天前
ios内存管理
ios·objective-c·swift·客户端开发
黑科技iOS上架1 天前
ios应用被封号后再次上架很难么?
经验分享·ios
柚鸥ASO优化1 天前
一篇讲透安卓ASO!开发者千万别只盯着iOS了
android·ios·aso优化