SwiftUIでのキーボードショートカット
SwiftUIではButtonやToggleに対して .keyboardShortcut(_:modifiers:) でキーボードショートカットを実装することができる。
Toggle("Toggle", isOn: $flag) .keyboardShortcut("1", modifiers: [.control]) Button("Button") { print("Control+2") } .keyboardShortcut("2", modifiers: [.control])
タブ単体では動かない
エディタアプリを作っているとTabキーで \t を入力させずに、インデント処理を行わせたい場合がある。そのため、上述したノリで、
Button("Button") { print("Tab") } .keyboardShortcut(.tab, modifiers: [])
と実装してみるが動かない。ちなみに、modifiers に色々と入れられるが、試してみて動いたのは Option + Tab の組み合わせのみだった。
解決方法
色々試したが、UITextView を UIViewRepresentable で利用する方法になった。エディタアプリを作る時にはSwiftUIの TextEditor では機能不足で、 UITextView を利用することが多いのでちょうどよいといえばちょうどよい。
まず、 keyCommands を登録したCustom UITextViewを用意する。
protocol MyTextViewDelegate: AnyObject { func myTextViewDidInputTab() } final class MyTextView: UITextView { override var keyCommands: [UIKeyCommand]? { let command = UIKeyCommand(title: "Shortcut Title", action: #selector(keyCommandTab(_:)), input: "\t", modifierFlags: []) // iOS 15以降ではこれを設定しないとEditor側への入力が優先される // ref: https://developer.apple.com/documentation/uikit/uikeycommand/3780513-wantspriorityoversystembehavior command.wantsPriorityOverSystemBehavior = true return [ command ] } weak var myDelegate: MyTextViewDelegate? @objc func keyCommandTab(_ sender: UIKeyCommand) { myDelegate?.myTextViewDidInputTab() } }
次にこれをSwiftUIで利用できるようにする。
struct TextView: UIViewRepresentable { private let onInputCommandTab = PassthroughSubject<Void, Never>() func makeCoordinator() -> Coordinator { return Coordinator(self) } func makeUIView(context: Context) -> MyTextView { let view = MyTextView() view.myDelegate = context.coordinator return view } func updateUIView(_ uiView: MyTextView, context: Context) { context.coordinator.parent = self } func onEvent(_ onInputCommandTab: (() -> Void)? = nil) -> some View { return onReceive(self.onInputCommandTab) { onInputCommandTab?() } } } extension TextView { final class Coordinator: NSObject, MyTextViewDelegate { fileprivate var parent: TextView init(_ parent: TextView) { self.parent = parent } func myTextViewDidInputTab() { parent.onInputCommandTab.send() } } }
これで使う準備は整った。実際に使う時にはこうする。
import SwiftUI struct ContentView: View { var body: some View { VStack() { TextView() .onEvent { print("Input Tab.") } } .padding() } }
これでTabを入力した時にはUITextViewに \t は入力されずに、イベントが発火して "Input Tab." がログ表示される。また、iPadで表示できるショートカット一覧にも、しっかりと登録されている。

これで無事にTabキーをハンドリングできるようになった。めでたし。