【Swift】Modifier キーがどうやって判別されているか
はじめに
こんにちは、 yu です。
最近ふと思いたって MacOS アプリを開発し始めたのですが、ユーザーがオリジナルのショートカットを作成できるようにしたく、 Modifier キー (command, option, control, etc...) の検知方法について調べていました。
そこで、 Modifier キーの検知方法が面白かったので記事にして共有したいと思います。
キーの検知方法
まず、普通のキーと Modifier キーを検知するためのコードを記述していきます。
結論から言うと、このような感じになります。
@FocusState private var isReadingKeys: Bool
...
Text("Read Keys...")
  .focusable()
  .focused($isReadingKeys)
  .focusEffectDisabled()
  .onTapGesture {
      isReadingKeys = true
  }
  .onKeyPress{ keyPress in
      print(keyPress.key)
      print(keyPress.modifiers)
      return .handled
  }
コード解説
- 
Text を配置する Text("Read Keys...")
- 
focusable にする フォーカスを有効にしておかないと、後述の onKeyPressModifier を追加してもキー検知ができません。@FocusState private var isReadingKeys: Bool // フォーカスの状態を格納 ... .focusable() // フォーカスを有効にする .focused($isReadingKeys) // フォーカスの状態を渡す .focusEffectDisabled() // フォーカスエフェクトを無効にできます
- 
onKeyPressModifier を追加する
 一番の肝ですね。ここでは、入力されたキーと Modifier キーを出力するようにします。.onKeyPress { keyPress in print(keyPress.key) print(keyPress.modifiers) return .handled }onKeyPressの詳細はこちら: Input events - Apple Developer Documents
実際に動かしてみる
- 
aを押した場合KeyEquivalent(character: "a")なるほど、こんな感じで出るんですね。 
- 
command + cを押した場合KeyEquivalent(character: "c") EventModifiers(rawValue: 16)ふむふむ… 
- 
command + shift + cを押した場合KeyEquivalent(character: "c") EventModifiers(rawValue: 20)ん…? 
 なんとなく予測はしていたけど、 Modifier キーの値が数値で出力されているから、値だけ見るとどの組み合わせかよく分からんな…
 EventModifiers の仕様
そこで一旦 EventModifiers 構造体の仕様を見てみましょう。
/// All possible modifier keys.
public static let all: EventModifiers
/// The Caps Lock key.
public static let capsLock: EventModifiers
/// The Shift key.
public static let shift: EventModifiers
/// The Control key.
public static let control: EventModifiers
/// The Option key.
public static let option: EventModifiers
/// The Command key.
public static let command: EventModifiers
/// Any key on the numeric keypad.
public static let numericPad: EventModifiers
EventModifiers - Apple Developer Documentation
なるほど、 EventModifiers には組み合わせとかが記述されてないんですね。(いっぱいあるからそりゃそうやんな笑)
じゃあどうやって Modifier キーの組み合わせを検知しているんだ…?
ドキュメントには一切書いてないので、とりあえず全部 rawValue を出力してみた。
.all        // 63
.capsLock   // off: 0, on: 1 (押下時だけではなく有効時なら常に +1 される)
.shift      // 2
.control    // 4
.option     // 8
.command    // 16
.numericPad // 32
!!!!!! 💡
これは!2のべき乗になっているではないか!
ドキュメントに書いといてくれ!!
Modifier キーの判定
ここまでは分かったものの自分の脳みそではロジックを構築しきれなかったので、助っ人 ChatGPT くんにロジックを書いてもらった。
(困ったら AI っていう流れ良くないな…思考力が確実に落ちてる気がする…)
func detectModifierKeys(_ number: Int) -> [Int] {
    var powersOfTwo: [Int] = []
    var power = 0
    var currentNumber = number
    
    while currentNumber > 0 {
        if currentNumber & 1 == 1 {
            powersOfTwo.append(1 << power) // Add 2^power to the list
        }
        currentNumber >>= 1 // Right shift the number to check the next bit
        power += 1
    }
    
    return powersOfTwo.reversed() // Optional: Return in descending order of powers
}
なるほど、一つ一つ紐解いていきましょう。
コード解説
何か例があった方がいいと思うので、仮に 26 (command + option + shift) で試してみます。
- 
変数定義 var powersOfTwo: [Int] = [] // 2の累乗の値を格納 ([2, 8, 16] が入るはず) var power = 0 // 現在が何乗かを格納 (ビット位置) var currentNumber = number // 現在の値 (Int)
- 
whileループ (1ループずつ丁寧に追っていきます)- 
1回目 while currentNumber > 0 { // 26 > 0 なので入る if currentNumber & 1 == 1 { // 26 (11010) & 1 (00001) = 0 (00000) なのでスキップ powersOfTwo.append(1 << power) // Skipped } currentNumber >>= 1 // 26 (11010) を右シフトすると 13 (01101) なので currentNumber = 13 power += 1 // 1回右シフトしたので power = 1 }
- 
2回目 while currentNumber > 0 { // 13 > 0 なので入る if currentNumber & 1 == 1 { // 13 (01101) & 1 (0001) = 1 (00001) なので入る powersOfTwo.append(1 << power) // 1 を 1 (power) 回シフトしたもの (= 2^1 = 2) を挿入 } currentNumber >>= 1 // 13 (01101) を右シフトすると 6 (00110) なので currentNumber = 6 power += 1 // 右シフトは2回目なので power = 2 }
- 
3回目 while currentNumber > 0 { // 6 > 0 なので入る if currentNumber & 1 == 1 { // 6 (00110) & 1 (00001) = 0 (00000) なのでスキップ powersOfTwo.append(1 << power) // Skipped } currentNumber >>= 1 // 6 (00110) を右シフトすると 3 (00011) なので currentNumber = 3 power += 1 // 右シフトは3回目なので power = 3 }
- 
4回目 while currentNumber > 0 { // 3 > 0 なので入る if currentNumber & 1 == 1 { // 3 (00011) & 1 (00001) = 1 (00001) なので入る powersOfTwo.append(1 << power) // 1 を 3 (power) 回シフトしたもの (= 2^3 = 8) を挿入 } currentNumber >>= 1 // 3 (00011) を右シフトすると 1 (00001) なので currentNumber = 1 power += 1 // 右シフトは4回目なので power = 4 }
- 
5回目 while currentNumber > 0 { // 1 > 0 なので入る if currentNumber & 1 == 1 { // 1 (00001) & 1 (00001) = 1 (00001) なので入る powersOfTwo.append(1 << power) // 1 を 4 (power) 回シフトしたもの (= 2^4 = 16) を挿入 } currentNumber >>= 1 // 1 (00001) を右シフトすると 0 (00000) なので currentNumber = 0 power += 1 // 右シフトは5回目なので power = 5 }
- 
最後 while currentNumber > 0 { // 0 > 0 なのでスキップ // Skipped }
 
- 
- 
結果返却 
 (2) で追っていったように、powerOfTwoには [2, 8, 16] が入っています。
 それをそのまま返却してもいいのですが、個人的に command と shift などが同時に押されている場合、 command が一番先頭に来てほしい感があるので、reversed()で逆順にして返却します。return powersOfTwo.reversed()これで動くはず! 
動作結果
下記のようなメソッドを作成して、ショートカットキーのラベルを生成します。
label には SF Symbols の command キーなどのアイコンを入れているのですが、記事に貼り付けるときに文字化けしてしまったのでテキストで代用しています。
func keyLabel(for key: String, with modifiers: Int) -> String {
    let modifierKeys = detectModifierKeys(modifiers)
    
    var label = ""
    
    for modifierKeyIndex in modifierKeys {
        switch EventModifiers(rawValue: modifierKeyIndex) {
        case .capsLock: label += "capsLock + "
        case .command: label += "command + "
        case .option: label += "option + "
        case .control: label += "control + "
        case .shift: label += "shift + "
        default: break
        }
    }
    label += key
    return label
}
すると…

そうそう!!これがしたかった!!!
おわりに
記事を最後までご覧いただきありがとうございます。
この仕組みには純粋に感動しました。 Windows とかもこんな仕組みになっているんだろうか…
これをなんでドキュメントに書かないんだって思う一方、こうやって仕組みを探っていくのも面白かったのでこれはこれでいいかなと思ってます笑
はじめにも少し書きましたが、 MacOS 向けの便利アプリを作っていますので、ある程度形になれば GitHub リンクをこの記事に追記しようと思います。
ではまた〜


Discussion