WWDC Quick Look 💓 By SwiftGGTeam
Support hardware keyboards in your app

Support hardware keyboards in your app

观看原视频

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:17UIKeyCommand 表示 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: " " 把空格键绑定到 playPause selector。
  • 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
    }
}

关键点:

  • UIKeyCommandUICommand 的子类。
  • buildMenu(with:) 是把命令接入菜单结构的入口。
  • replaceChildren(ofMenu:) 可以替换或补充指定菜单的子项。
  • 对 Mac Catalyst App 来说,键盘快捷键和菜单项应该指向同一套命令模型。

3. 让 table view 和 collection view 支持键盘辅助多选

06:28)如果 App 有文件列表,用户会期待 Shift 点击选择连续项目,Command 点击扩展非连续选择。UITableViewUICollectionView 已经提供对应的多选回调。

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:00UIKeyCommand 只在快捷键被触发时调用一次。方向键移动画布对象这类交互需要区分 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 可以让同一个拖拽手势产生更精确的编辑行为。 怎么开始:在 UIPanGestureRecognizer callback 中读取 modifierFlags,按 .shift.command 切换保持宽高比、吸附网格或多选模式。

  • 做什么:用方向键连续移动画布对象。 为什么值得做UIKeyCommand 只触发一次,连续移动需要 key down 和 key up 两个阶段。 怎么开始:在画布 view controller 中覆写 pressesBeganpressesEnded,根据 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