本文介绍 UIResponder 的职责、触摸/按压/摇动/远程控制等事件类型、First Responder 的获取与交出,以及输入视图、编辑菜单等与响应者链相关的机制。与 hit-test 和链的构成可参见 02-hitTest 与事件传递、03-响应者链与 nextResponder。
一、UIResponder 的定位
UIResponder 是 UIKit 中事件处理与传递的抽象基类。常见子类包括 [1]:
- UIApplication
- UIViewController
- UIView(含 UIWindow、UIControl、UILabel 等)
响应者负责:接收事件(触摸、按压、摇动、远程控制等)、处理或转发给 next,以及第一响应者的争夺与交出(如键盘焦点、编辑菜单)。
二、事件类型与第一响应者
系统根据事件类型决定「第一响应者」是谁 [2]:
| 事件类型 | 第一响应者通常为 |
|---|---|
| 触摸事件 | hit-test 得到的视图 |
| 按压事件(Press) | 当前拥有焦点的对象 |
| 摇动(Shake) | 由系统或开发者指定的对象 |
| 远程控制 | 由系统或开发者指定的对象 |
| 编辑菜单(复制/粘贴等) | 由系统或开发者指定的对象 |
注意:与加速度计、陀螺仪、磁力计相关的运动数据不经过响应者链,由 Core Motion 直接投递给指定对象 [[2]]。
2.1 泳道图:事件类型与第一响应者确定
flowchart LR
subgraph 事件来源
E1[触摸]
E2[按键/按压]
E3[摇动/远程]
end
subgraph 系统
S1[hit-test]
S2[焦点/指定]
end
subgraph 第一响应者
R1[命中 View]
R2[当前焦点]
R3[指定对象]
end
E1 --> S1
E2 --> S2
E3 --> S2
S1 --> R1
S2 --> R2
S2 --> R3
三、触摸事件方法
响应者通过重写以下方法处理触摸 [[1]]:
| 方法 | 含义 |
|---|---|
touchesBegan(_:with:) | 一个或多个手指刚接触屏幕 |
touchesMoved(_:with:) | 触摸在屏幕上移动 |
touchesEnded(_:with:) | 一个或多个手指离开屏幕 |
touchesCancelled(_:with:) | 系统取消触摸序列(如来电、弹窗) |
touchesEstimatedPropertiesUpdated(_:) | 预估属性更新(如 force、altitude 等) |
默认实现会将事件转发给 next。若重写后不调用 super 或 next?.touchesXxx(...),链会在此中断。
商用场景示例:自定义涂鸦/签名 view,需完整追踪触摸轨迹,重写 touchesBegan/Moved/Ended 在自身 layer 上绘制,并在结束时调用 next?.touchesEnded(...) 以便上层做提交、保存等;或可拖拽的卡片 view 在 touchesMoved 中更新 frame,不消费时交给 next 做滚动。
完整触摸处理示例(Swift):
class DraggableCardView: UIView {
private var startCenter: CGPoint = .zero
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
startCenter = center
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let now = touch.location(in: superview)
let prev = touch.previousLocation(in: superview)
center = CGPoint(x: center.x + (now.x - prev.x), y: center.y + (now.y - prev.y))
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// 可选:交给链上处理
next?.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
center = startCenter
next?.touchesCancelled(touches, with: event)
}
}
四、按压、摇动与远程控制
4.1 按压事件(Press)
用于物理按键(如外接键盘、Apple TV 遥控器)。需重写 [[1]]:
pressesBegan(_:with:)pressesChanged(_:with:)pressesEnded(_:with:)pressesCancelled(_:with:)
4.2 摇动(Motion)
设备摇动时触发。重写:
motionBegan(_:with:)motionEnded(_:with:)motionCancelled(_:with:)
4.3 远程控制(Remote Control)
耳机、锁屏界面等控制的播放/暂停等。重写:
remoteControlReceived(with:)
商用场景示例:音乐/播客 App 在后台时,锁屏或耳机线控的播放/暂停/上一曲/下一曲通过远程控制事件交给当前第一响应者或 App 指定 VC;在根 VC 中重写 remoteControlReceived(with:) 并调用播放器接口即可。
五、First Responder 管理
5.1 相关 API
| 属性/方法 | 作用 |
|---|---|
canBecomeFirstResponder | 是否允许成为第一响应者(默认 false,需子类按需重写为 true) |
becomeFirstResponder() | 请求成为第一响应者 |
canResignFirstResponder | 是否允许交出第一响应者 |
resignFirstResponder() | 交出第一响应者 |
isFirstResponder | 当前是否为第一响应者 |
例如 UITextField:点击后通过 becomeFirstResponder() 成为第一响应者并弹出键盘;键盘的 inputView 会与该响应者关联。
5.2 输入视图(Input View)
| 属性 | 说明 |
|---|---|
| inputView | 成为第一响应者时显示的视图(如自定义键盘) |
| inputAccessoryView | 输入视图上方的附件视图(如工具栏) |
系统键盘即为 UITextField/UITextView 的默认 inputView;可替换为自定义 view,仍通过响应者链与第一响应者关联。
商用场景示例:安全输入场景下使用自定义数字/安全键盘(inputView)替代系统键盘,防止第三方键盘截获;或电商/IM 输入框上方挂快捷短语/商品推荐条(inputAccessoryView),点击后插入内容。
First Responder 完整示例(Swift,支持成为第一响应者并弹出自定义输入视图):
class CustomInputView: UIView { /* 自定义键盘 UI */ }
class SecureTextField: UITextField {
override var inputView: UIView? { customKeyboard }
private let customKeyboard = CustomInputView()
override var canBecomeFirstResponder: Bool { true }
}
六、编辑菜单与 canPerformAction
编辑菜单(复制、粘贴、剪切等)会在响应者链上查找能执行对应 selector 的响应者。可重写 [[1]]:
- canPerformAction(_:withSender:):是否在链上显示并启用该命令
- target(forAction:withSender:):指定执行该 action 的 target
从而实现「当前选中的 view 或 VC 提供复制/粘贴」等行为。
商用场景示例:富文本/笔记详情页中,长按选中后弹出系统编辑菜单(复制/粘贴/剪切);在负责该内容的 ViewController 中实现 copy(_:)、paste(_:),并重写 canPerformAction(_:withSender:) 根据当前选中内容决定是否显示「复制」或「粘贴」。
编辑菜单示例(Swift):
class NoteDetailViewController: UIViewController {
var selectedText: String?
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(copy(_:)) { return selectedText != nil && !(selectedText?.isEmpty ?? true) }
if action == #selector(paste(_:)) { return UIPasteboard.general.hasStrings }
return super.canPerformAction(action, withSender: sender)
}
override func copy(_ sender: Any?) {
guard let text = selectedText else { return }
UIPasteboard.general.string = text
}
override func paste(_ sender: Any?) {
guard let str = UIPasteboard.general.string else { return }
insertText(str)
}
}
七、思维导图小结
mindmap
root((UIResponder 与事件))
触摸
touchesBegan / Moved / Ended / Cancelled
hit-test view 为第一响应者
其他事件
Press / Motion / Remote Control
第一响应者由焦点或指定
First Responder
become / resign
inputView / inputAccessoryView
编辑菜单
canPerformAction
target forAction
参考文献
[1] UIResponder | Apple Developer Documentation
[2] Using responders and the responder chain to handle events - Determine an event's first responder