View Controller でキーイベント処理をする

Qiita に移行しました。 https://qiita.com/morikiyo/items/208cc66215d00cc10e76

環境

前提

  • テキストボックスなど、キー入力を処理する View がないこと

サンプルコード

override func keyDown(with event: NSEvent) {
    let s = event.characters ?? "(nil)"
    print(#function + ": " + s)
}

override func viewDidAppear() {
    super.viewDidAppear()
    
    if let window = self.view.window {
        window.makeFirstResponder(self)
    }
}

説明

キー入力イベントをレスポンダチェーンで拾うには NSResponder#keyDown などのキーイベントメソッドをオーバーライドすればよい

NSViewController#keyDown をオーバーライドしてもキー入力に反応しないというブログや質問/回答を散見したが、原因はおそらく、First Responder になっていないからだと思われる (環境によるかもしれないので断定はできない)

なので、サンプルコードでは View が表示されたときに (viewDidAppear で) 明示的に NSWindow#makeFirstResponder を呼び出して、View Controller 自身を First Responder に指定している (ちなみに、makeFirstResponder を処理するのに acceptsFirstResponder -> true である必要はない)

First Responder がキーイベントを処理する Responder に移ったとき (例えば、テキストボックスが Window 内にあり、そこにフォーカスされたときなど) には View Controller のキーイベントメソッドは処理されなくなるので注意

ラベルやボタンなど、キー入力を処理しない View が First Responder であれば、レスポンダチェーンをたどり View Controller のイベントメソッドが処理されるが、テキストボックスなどは自身のイベントメソッドを処理してレスポンダチェーンを終了してしまうためだ (たぶん)

Window 内のどの View がキーイベントを処理中でも、キーイベントを検知したいというのであれば、それはレスポンダチェーンではなく (NSResponder のメソッドをオーバーライドするのではなく)、NSEvent#addLocalMonitorForEvents などを利用してイベントハンドラを追加すべきかと思う

参考

調査メモ

  • NSWindow#makeFirstResponder を呼び出さないとき、画面表示時の First Responder -> Window だった
  • acceptsFirstResponder は、タブキーなどでコントロールのフォーカスを移す際に、フォーカスが移せるかどうかの判定に使用されるものっぽい (調査不足)
  • マウスクリックイベントのレスポンダチェーンは NSWindow#firstResponder から始まるのではなく、クリックイベントの発生した座標に存在する View から始まるのではないかと思うが、詳細不明 (調査不足)