目次
Session概要
あなたのアプリで外部キーボードが使えると、より体感的で馴染みのあるタイピング体験を提供できるだけでなく、素早いナビゲーションやキーボードショートカットを利用することもできるようになります。あなたのiPadOSやMac Catalyst Appで、外部キーボードをサポートする最適な方法を学びましょう。
レスポンダチェーンへの理解を深め、カスタムキーボードショートカットを実装する際のベストプラクティスについて。
一般的なシステムキーボードショートカットを導入・実行し、ジェスチャ認識でモディファイアを利用、未処理のキーボードイベントAPIを活用、key downとkey upイベントを容易に対応する方法についてなど。 https://developer.apple.com/videos/play/wwdc2020/10109/
背景/外部キーボード対応する動機
- iPadを利用するユーザのキーボードを利用することがかなり一般的となってきた
- アプリによってはキーボードを利用することでユーザの生産性が上がり、UIをより迅速に人間工学的に操作できるようになる
- また、標準システムのショートカットの実装により馴染みのある一貫したインターフェイスをアプリにアプローチできます
- 上記から、iPad向けアプリでより良いUI体験を実現するにはタッチ操作とハードウェアキーボード操作の両方で優れている必要がある

キーボードショートカットについて
- UIKeyboardCommand
- カスタムキーボードショートカットを表すオブジェクト
- ユーザに表示される
discoverbilityTitle
などのプロパティやオブジェクトを呼び出すために必要なキーボード入力、またオプションで押される修飾キーを定義するmodifierFlags
のセットなどを持っている
- UIResponderの
keyCommands
- キーコマンドの配列を返すことでUIと連携して機能する
- キーコマンドを含めるためにアプリを拡張するために、overrideしUIの特定の部分に関連するキーコマンドを返すだけ


レスポンダチェーン
- アプリのView階層に従い、ユーザが対話しているレスポンダが最初(UIResponder)にあり、最後にUIApplicationとなっている
- チェーンでレスポンダが特定のイベントを処理できない場合、イベントはチェーンのさらに上へいく

キーボードショートカット一覧の提供
- システムにより全てのキーコマンドが集約されると、ユーザは見つけやすいHUD(Head Up Display)で一覧を見ることができる
- HUDはシステム内の任意の場所でコマンドキーを長押しすることでアクセス可能
- 開発中にテストおよび検査するのに便利な機能

キーボードショートカットの実装
- キーボードショートカットの動作を定義するための理想的な場所はカスタムViewControllerのサブクラス
- UIViewControllerはUIResponderのサブクラスであるため、いくつかのメソッドをoverrideするだけでキーボードショートカットの受け入れを開始できる
- 全てのUIResponderは
UIResponderStanderdEditActions
と呼ばれるプロトコルに準拠しており、下記の画像にあるリストの任意のメソッドに応えます- そのため、単一のUIKeyCommandの作成は不要で、overrideするだけでよい
class PlayerViewController: UIViewController {
override var canBecomeFirstResponder: Bool {
return true
}
override func viewDidAppear(_ animated: Bool) {
becomeFirstResponder()
}
// カスタムのスペースバー キーボードショートカットを設定する例
// この実装をすることでHUDに表示されるので、ローカライズも行っている
override var keyCommands: [UIKeyCommand]? {
return [
UIKeyCommand(title: NSLocalizedString("PLAY_PAUSE", comment: "…"),
action: #selector(playPause),
input: " ")
]
}
}

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?) { … }
}
Catalystで動作させる
- UIKeyCommandはCatalystを念頭に設計されているため、新しいキーコマンドは簡単にmacOSのメニューバーで機能させられる
- UICommandは
command builder API
に簡単に統合できる
class UIKeyCommand : UICommand {
...
}
override func buildMenu(with builder: UIMenuBuilder) {
builder.replaceChildren(ofMenu: .file) { children in
return [ UIKeyCommand() ] + children
}
}
キーボードがTableView/CollectionViewと作用する方法
- アプリでリスト表示しているViewがある場合、ユーザはmacOSのアプリと同様の機能があると考える
- 例えば、Shiftキーを押しながらリストの連続する項目を選択できる機能やCmdキーを押しながらリストの複数の項目をタップするとユーザの選択を拡張できる機能など
- これらの実装は下記のようなSampleコードで簡単に実装可能


// trueを返すようにするだけで、システムは自動的にTableViewを編集モードにするか、
// CollectionViewを複数選択モードにし、押されている修飾キーに基づき
// ユーザが現在選択しているIndexPathのセットを拡張する
optional func tableView(_ tableView: UITableView,
shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool
// 選択した項目の編集モードにすることが可能
optional func tableView(_ tableView: UITableView,
didBeginMultipleSelectionInteractionAt indexPath: IndexPath)
iOS 14のジェスチャ認識における新しい機能
- commandキーを押しながら複数の項目を選択することや、Shiftキーを押しながらオブジェクトのサイズを変更することは iOS13.4でUIGestureRecognizerに
modifierFlags
プロパティを追加したことで可能になった - 詳細は Handle trackpad and mouse input のセッションを参照
func recognizedDragGesture(_ panGesture: UIPanGestureRecognizer) {
if panGesture.modifierFlags.contains(.command) {
snapToGrid = true
} else if panGesture.modifierFlags.contains(.shift) {
constrainAspectRatio = true
}
...
}
Rawキーボードイベントの応答
- ユーザがキーボードショートカットだけでなく十字キーを操作してアプリを操作したい場合、UICommandでは実現できない
- 上記を実現するために、
key down
およびkey up
イベントに応答する機能が追加された
class UIResponder: NSObject {
// 全てのキーボードコマンドは下記メソッドを経由する
// キーが押された時に通知を受け取る
func pressesBegan(_ presses: Set<UIPress>,
with event: UIPressesEvent)
// キーが離された時に通知を受け取る
func pressesEnded(_ presses: Set<UIPress>,
with event: UIPressesEvent)
}
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()
}
}
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()
}
}
}
}