04-主题|事件响应者链@iOS-UIResponder与触摸及多类事件详解

5 阅读5分钟

本文介绍 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。若重写后不调用 supernext?.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