SwiftUIでbuttonStyleを適用したButtonに対してキーボードショートカットを有効にする

SwiftUIでbuttonStyleを適用したButtonに対してキーボードショートカットを有効にする
目次

SwiftUIで凝ったUIを作ろうとすると節々詰まることがあります。今回はデザインをカスタマイズしたButtonに対してキーボードショートカットを割り当てる方法を紹介します。

環境情報

今回コードを書いた環境はこちらです。

  • mac OSX 11.0
  • XCode 12.0

やりたいこと

やりたいことを箇条書きにすると以下です。

  • SwiftUIを使った
  • macOSアプリで
  • EnterキーでButtonをSubmitできるようにし
  • Buttonは buttonStyle modifierでデザインをカスタマイズする

入力フォームでよく使われる便利系の機能ですね。

Enter キーでSubmitができないパターン

実は macOS 11.0から KeyboardShortcut が提供されています。 以下のコードサンプルをご覧ください。 .keyboardShortcut modifier を使用し、任意のキーボード入力をショートカットとして設定できます。 指定されている .defaultAction はデフォルト値で、Returnキーです。このコードはショートカットが問題なく動作します。

1Button(action: {
2  debugPrint("Sign In")
3}) {
4  Text("Sign In")
5}
6.keyboardShortcut(.defaultAction)

しかし、以下のコードはショートカットが機能しません。なお、 buttonStyle に指定されている XXXXButtonStyle は任意のButtonStyle Structとします。

1Button(action: {
2  debugPrint("Sign In")
3}) {
4  Text("Sign In")
5}
6.keyboardShortcut(.defaultAction)
7.buttonStyle(XXXXButtonStyle())

StackOverflow 上にも似た症状で困っている方がいるのを発見しました。

解決策:ZStackでキーボードショートカット専用ボタンを隠す

ワークアラウンドの結果、1つのButtonに対してkeyboardShortcutとbuttonStyleの併用すると、キーボードショートカットのみ効かなくなる ということが判明しました。 そのため、SwiftUIの挙動が改善されるまでは回避策を自作する必要があります。 一番手軽だった方法を紹介すると「キーボードショートカット専用のボタンを作成し、ZStackで隠す」です。

 1ZStack {
 2  // Button for handling keyboard shortcut
 3  Button(action: {
 4    debugPrint("Sign In")
 5  }) {}
 6    .padding(0)
 7    .opacity(0)
 8    .frame(width: 0, height: 0)
 9    .keyboardShortcut(.defaultAction)
10  // Button for handling mouse event
11  Button(action: {
12    debugPrint("Sign In")
13  }) {
14    Text("Sign In")
15  }
16  .buttonStyle(XXXXButtonStyle())
17}

ZStackは後に定義したViewが前面に表示されるため、.buttonStyle を適用したButtonの裏側にショートカットキーをハンドリングする用のButtonを隠してしまいます。 これでショートカットが効きます。

ポイント1: ショートカット専用ボタンは限りなく小さく、かつ透過に

重要なポイントとしては、 .frame(width: 0, height: 0).padding(0) で可能な限りサイズを小さくした後、 .opacity(0) で完全透過にすることです。 .opacity(0) は前面のButtonに透過を付与した時に裏側のボタンが透けて見えてしまう問題を回避するのに役立ちます。

ポイント2: hidden() modifier は使えない

一見 hidden() modifier を指定すれば裏側のボタンを綺麗に隠せると考える人もいるかもしれません。 しかし、 hidden() を指定するとキーボードショートカットが機能しなくなってしまいます。

まとめ

SwiftUI で デザインをカスタムしたボタンに対してキーボードショートカットを割り当てる場合には

  • ZStack で Button を2つ作成する
  • 片方には .buttonStyle を適用し、もう一方には .keyboardShortcut を指定する
  • Buttonのaction部には同じ処理を記述する

で実装できます。

ショートカットの箇所が増えるようならmodifierを自作するのもありです。

バッドノウハウ感全開ですが誰かのお役に立てば。

参考にさせていただいたサイト