Highlight
UIKit 的硬件键盘支持建立在 responder chain 上:App 可以用 UIKeyCommand 与 UIResponderStandardEditActions 提供可发现快捷键,用 UITableView/UICollectionView 的多选回调响应 Shift/Command 选择,用 UIGestureRecognizer.modifierFlags 和 pressesBegan/pressesEnded 处理修饰键手势与按键按下/释放事件。
核心内容
iPad App 最初围绕触摸设计。按钮显示在屏幕上,用户点一下就能发现功能。外接硬件键盘进入日常使用后,另一个预期出现了:用户希望用空格控制播放,用 Command 发现快捷键,用 Shift 或 Command 扩展列表选择,用方向键微调画布对象。
这场 session 先解释事件从哪里来。键盘事件会进入 responder chain。当前 first responder 最先收到事件;如果它不能处理,事件继续向上走。UIKit 会沿着这条链收集每个 responder 暴露的 keyCommands,再把它们展示在长按 Command 键出现的 discoverability HUD 中。
随后,演讲把键盘支持拆成四层。第一层是自定义快捷键,用 UIKeyCommand 连接输入和 selector。第二层是标准编辑动作,selectAll(_:)、copy(_:)、paste(_:) 这些方法不需要额外创建快捷键。第三层是列表和集合视图的多选,系统会根据 Shift 或 Command 扩展选区。第四层是更底层的输入,gesture recognizer 可以读取修饰键,UIResponder 可以直接响应 key down 和 key up。
这套模型也照顾 Mac Catalyst。UIKeyCommand 继承自 UICommand,可以进入 command builder API,显示在 macOS 菜单栏里。开发者不用为 iPad 和 Mac 维护两套完全分离的命令入口。
详细内容
1. 用 UIKeyCommand 暴露自定义快捷键
(01:17)UIKeyCommand 表示 App 自己定义的键盘快捷键。它包含 discoverability title、触发输入、可选的 modifier flags,以及被调用时执行的 action。UIKit 通过 UIResponder.keyCommands 从 responder chain 收集这些命令。
(03:14)演讲用 Music 的播放控制做例子。播放器 view controller 成为 first responder 后,空格键就能触发 playPause。
class PlayerViewController: UIViewController {
override var canBecomeFirstResponder: Bool {
return true
}
override func viewDidAppear(_ animated: Bool) {
becomeFirstResponder()
}
override var keyCommands: [UIKeyCommand]? {
return [
UIKeyCommand(title: NSLocalizedString("PLAY_PAUSE", comment: "…"),
action: #selector(playPause),
input: " ")
]
}
}
关键点:
canBecomeFirstResponder返回true,这个 view controller 才能成为键盘事件入口。viewDidAppear(_:)调用becomeFirstResponder(),让播放器界面出现后立即接收快捷键。keyCommands返回当前界面可用的命令数组。input: " "把空格键绑定到playPauseselector。title用本地化字符串,系统会在长按 Command 键时展示给用户。
2. 标准编辑动作不需要手写 UIKeyCommand
(04:31)很多快捷键已经有系统约定。音乐库或文件列表常见的 select all、copy、paste,属于 UIResponderStandardEditActions。只要在 responder 子类里覆写对应方法,UIKit 就能把常见编辑快捷键接到这些动作上。
class SongListTableViewController: UITableViewController {
override var canBecomeFirstResponder: Bool {
return true
}
override func viewDidAppear(_ animated: Bool) {
becomeFirstResponder()
}
/* UIResponderStandardEditActions */
override func selectAll(_ sender: Any?) { … }
override func copy(_ sender: Any?) { … }
override func paste(_ sender: Any?) { … }
}
关键点:
UITableViewController也是UIResponder,可以直接参与 responder chain。selectAll(_:)、copy(_:)、paste(_:)来自UIResponderStandardEditActions。- 使用这些标准动作时,不需要为每个动作创建
UIKeyCommand。 - 这种写法让用户从 Mac 带来的 Command-A、Command-C、Command-V 预期在 iPad App 中成立。
(05:52)自定义命令进入 Mac Catalyst 菜单栏时,可以把 UIKeyCommand 当作 UICommand 放进 command builder。
class UIKeyCommand : UICommand {
...
}
override func buildMenu(with builder: UIMenuBuilder) {
builder.replaceChildren(ofMenu: .file) { children in
return [ UIKeyCommand() ] + children
}
}
关键点:
UIKeyCommand是UICommand的子类。buildMenu(with:)是把命令接入菜单结构的入口。replaceChildren(ofMenu:)可以替换或补充指定菜单的子项。- 对 Mac Catalyst App 来说,键盘快捷键和菜单项应该指向同一套命令模型。
3. 让 table view 和 collection view 支持键盘辅助多选
(06:28)如果 App 有文件列表,用户会期待 Shift 点击选择连续项目,Command 点击扩展非连续选择。UITableView 和 UICollectionView 已经提供对应的多选回调。
optional func tableView(_ tableView: UITableView,
shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool
optional func tableView(_ tableView: UITableView,
didBeginMultipleSelectionInteractionAt indexPath: IndexPath)
关键点:
shouldBeginMultipleSelectionInteractionAt返回true后,系统开始处理多选交互。- table view 会进入 editing mode,collection view 会进入 multiple selection mode。
- 系统根据用户按下的 Shift 或 Command 扩展当前选区。
didBeginMultipleSelectionInteractionAt适合更新周边 UI,例如显示批量操作栏。
4. 在 gesture recognizer 中读取键盘修饰键
(07:47)iOS 13.4 增加了 UIGestureRecognizer.modifierFlags。它记录 gesture recognizer 状态变化时用户按住了哪些键盘修饰键。Numbers 的例子是:按住 Shift 调整形状时保持宽高比,按住 Command 选择多个对象。
func recognizedDragGesture(_ panGesture: UIPanGestureRecognizer) {
if panGesture.modifierFlags.contains(.command) {
snapToGrid = true
} else if panGesture.modifierFlags.contains(.shift) {
constrainAspectRatio = true
}
...
}
关键点:
modifierFlags来自当前 gesture recognizer,不需要单独监听键盘。.command可以切换到网格吸附这类精确编辑模式。.shift可以切换到保持宽高比这类约束模式。- 这类交互适合绘图、排版、表格、地图编辑等专业场景。
5. 用 pressesBegan 和 pressesEnded 响应原始按键事件
(09:00)UIKeyCommand 只在快捷键被触发时调用一次。方向键移动画布对象这类交互需要区分 key down 和 key up:按下时开始移动,松开时停止移动。新的原始键盘事件入口是 UIResponder.pressesBegan(_:with:) 和 UIResponder.pressesEnded(_:with:)。
class UIResponder: NSObject {
func pressesBegan(_ presses: Set<UIPress>,
with event: UIPressesEvent)
func pressesEnded(_ presses: Set<UIPress>,
with event: UIPressesEvent)
}
关键点:
pressesBegan在硬件键盘按键按下时调用。pressesEnded在按键释放时调用。- 这两个方法适合连续动作,不适合只触发一次的菜单命令。
- 原始按键事件仍然走 responder chain,所以放在 view 或 view controller 中都可以。
(09:59)画布示例在 pressesBegan 中检查方向键,在 pressesEnded 中停止移动。
class CanvasViewController: UIViewController {
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
for press in presses {
guard let key = press.key else { continue }
switch key.keyCode {
case .keyboardUpArrow: startMoveUp()
case .keyboardDownArrow: startMoveDown()
…
}
}
}
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
stopMoving()
}
}
关键点:
press.key可能为空,所以先用guard跳过没有 key 的 press。key.keyCode能区分上、下等具体按键。startMoveUp()和startMoveDown()代表按下方向键后的连续动作。stopMoving()放在pressesEnded,保证用户松开按键后动作停止。
(10:29)原始按键事件也能读取修饰键。下面的示例在移动对象时检查 Shift,用于切换到“边移动边选择”的行为。
class CanvasViewController: UIViewController {
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
var selectWhileMoving = false
for press in presses {
guard let key = press.key else { continue }
if key.modifierFlags.contains(.shift) {
selectWhileMoving = true
}
switch key.keyCode {
case .keyboardUpArrow: startMoveUp()
}
}
}
}
关键点:
key.modifierFlags.contains(.shift)直接读取当前按键事件的修饰键状态。selectWhileMoving是业务状态,表示移动时同时扩展选择。- 这和 gesture recognizer 的
modifierFlags是同一个交互思想:硬件键盘负责切换模式,触摸、指针或方向键负责执行动作。
核心启发
-
做什么:给媒体播放器增加空格键播放/暂停。 为什么值得做:session 明确用 Music 说明空格键是媒体播放 App 的常见预期。 怎么开始:让播放界面的 view controller 成为 first responder,在
keyCommands中返回一个UIKeyCommand,把input: " "连接到播放状态切换方法。 -
做什么:让文件、歌曲或项目列表支持 Command-A、Command-C、Command-V。 为什么值得做:
UIResponderStandardEditActions可以接入用户熟悉的标准编辑快捷键,减少自定义命令数量。 怎么开始:在列表控制器中覆写selectAll(_:)、copy(_:)、paste(_:),并确认该控制器能成为 first responder。 -
做什么:为列表加上 Shift/Command 辅助多选。 为什么值得做:桌面用户会自然期待 Shift 选择连续项目、Command 扩展非连续选择,UIKit 已经提供 table view 与 collection view 的多选入口。 怎么开始:实现
shouldBeginMultipleSelectionInteractionAt并返回true,再在didBeginMultipleSelectionInteractionAt中显示批量操作 UI。 -
做什么:给绘图或排版工具增加修饰键模式。 为什么值得做:演讲里的 Numbers 示例说明,Shift 和 Command 可以让同一个拖拽手势产生更精确的编辑行为。 怎么开始:在
UIPanGestureRecognizercallback 中读取modifierFlags,按.shift、.command切换保持宽高比、吸附网格或多选模式。 -
做什么:用方向键连续移动画布对象。 为什么值得做:
UIKeyCommand只触发一次,连续移动需要 key down 和 key up 两个阶段。 怎么开始:在画布 view controller 中覆写pressesBegan和pressesEnded,根据key.keyCode启动移动,根据按键释放停止移动。
关联 Session
- Handle trackpad and mouse input — 处理 iPad 与 Mac Catalyst 中的触控板、鼠标、滚动、修饰键和间接输入事件。
- Build for the iPadOS pointer — 设计和实现 iPadOS pointer interaction,让外接键盘、鼠标和触控板用户获得更自然的控件反馈。
- Designed for iPad — 从布局、导航、拖放、键盘和触控板角度设计更完整的 iPad App。
- Advances in UICollectionView — 学习 collection view 的列表、outline 和现代 cell 配置,为键盘多选列表提供更好的基础。
- What’s new in Mac Catalyst — 了解 Mac Catalyst 的生命周期、扩展、macOS 外观和 Optimized for Mac 模式。
评论
GitHub Issues · utterances