キーボードの押下状態を取得する(macOS/SwiftUI)

## 概要
- macOS/SwiftUIでキーボードの状態を取得する実装です。
- 今回はShiftキーや押されているキーの取得、また`Shift+a`の押下状態を検知します。

![image](https://i.imgur.com/zflKzDC.png)

## 参考
- [OSX アプリで option キーを押しながらボタンが押されたことを検出する](https://ez-net.jp/article/2C/B6FENipJ/H4-SjMamgl6l/)
- [SwiftUI\(macOS\)でキーボード入力を取得してタイピングゲーム機能を実現したいです。](https://teratail.com/questions/5k8hem1m3lks1f)

## 実装
- [keyDown\(with:\)](https://developer.apple.com/documentation/appkit/nsresponder/1525805-keydown)など`NSResponder`のイベントをカスタムの`NSView`で処理します。
    - イベントはSwiftUIのクラス側で設定できるように渡しています。
    - また[NSResponder](https://developer.apple.com/documentation/appkit/nsresponder)にあるように、キーボードのイベント以外にもマウスのイベントなども同様に実装することができそうです。
- カスタムした`NSView`は`NSViewRepresentable`を使ってSwiftUI側から使用します。
- [flagsChanged\(with:\)](https://developer.apple.com/documentation/appkit/nsresponder/1527647-flagschanged)は修飾キー(modifier key)の押下状態が変更された場合に呼ばれます。
- [keyDown\(with:\)](https://developer.apple.com/documentation/appkit/nsresponder/1525805-keydown)や[keyUp\(with:\)](https://developer.apple.com/documentation/appkit/nsresponder/1527436-keyup)はそれぞれキーボードが押される/押されなくなる、場合に呼ばれます。
    - また`shift`キーを単独で押した場合`keyDown`は呼ばれないため、`flagsChanged`側で処理する必要があります。

```swift
struct KeyEventHandling: NSViewRepresentable {
    
    // 親ビューから渡される
    var flagsChangedHandler: ((NSEvent) -> ())?
    var keyDownHandler: ((NSEvent) -> ())?
    var keyUpHandler: ((NSEvent) -> ())?
    
    class KeyView: NSView {
        
        var flagsChangedHandler: ((NSEvent) -> ())?
        var keyDownHandler: ((NSEvent) -> ())?
        var keyUpHandler: ((NSEvent) -> ())?
                
        override func awakeFromNib() {
            super.awakeFromNib()
        }
        
        override var acceptsFirstResponder: Bool {
            true
        }
        
        override func flagsChanged(with event: NSEvent) {
            super.flagsChanged(with: event)
            flagsChangedHandler?(event)
        }
                        
        override func keyDown(with event: NSEvent) {
            super.keyDown(with: event)
            keyDownHandler?(event)
        }
        
        override func keyUp(with event: NSEvent) {
            super.keyUp(with: event)
            keyUpHandler?(event)
        }
    }
    
    func makeNSView(context: Context) -> NSView {
        let view = KeyView()
        view.flagsChangedHandler = flagsChangedHandler
        view.keyDownHandler = keyDownHandler
        view.keyUpHandler = keyUpHandler
        
        DispatchQueue.main.async { // wait till next event cycle
            view.window?.makeFirstResponder(view)
        }
        return view
    }

    func updateNSView(_ nsView: NSView, context: Context) {
    }
}
```

## 呼び出し側
- `.background`経由で定義した`KeyEventHandling`を使用します。

```Swift
import SwiftUI

struct ContentView: View {
    
    @State private var modifierFlags: NSEvent.ModifierFlags = []
    @State private var keyDownCharacter: String? = nil
    @State private var shiftAndAKeyDown: Bool = false
    
    var body: some View {
        statusView()
            .background(KeyEventHandling(
                flagsChangedHandler: { event in
                    modifierFlags = event.modifierFlags
                },
                keyDownHandler: { event in
                    keyDownCharacter = event.characters
                    
                    // shift + a
                    shiftAndAKeyDown = (event.modifierFlags.contains(.shift) && event.keyCode == 0)
                },
                keyUpHandler: { event in
                    // 設定のリセット
                    keyDownCharacter = nil
                    shiftAndAKeyDown = false
                }))
    }
    
    @ViewBuilder
    private func statusView() -> some View {
        Grid(verticalSpacing: 8) {
            GridRow {
                Text("Shift:")
                    .gridColumnAlignment(.trailing)
                if modifierFlags.contains(.shift) {
                    Image(systemName: "checkmark.circle")
                        .imageScale(.large)
                        .foregroundColor(.green)
                } else {
                    Image(systemName: "x.circle")
                        .imageScale(.large)
                        .foregroundColor(.gray)
                }
            }
            
            GridRow {
                Text("Keydown:")
                Text(keyDownCharacter ?? "(nil)")
                    .frame(minWidth: 40)
            }
            
            GridRow {
                Text("Shift + a:")
                if shiftAndAKeyDown {
                    Image(systemName: "checkmark.circle")
                        .imageScale(.large)
                        .foregroundColor(.green)
                } else {
                    Image(systemName: "x.circle")
                        .imageScale(.large)
                        .foregroundColor(.gray)
                }
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```