hatenablogから2018-12-17の記事を移行しました。
https://bitsandbobs.hatenablog.jp/entry/2018/12/17/181419
環境
- macOS High Sierra 10.13.6
- Xcode 10.1
前提
- テキストボックスなど、キー入力を処理する 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 などを利用してイベントハンドラを追加すべきかと思う
参考
- Event Architecture
- NSResponder - AppKit | Apple Developer Documentation
- NSWindow - AppKit | Apple Developer Documentation
- NSEvent - AppKit | Apple Developer Documentation
調査メモ
- NSWindow#makeFirstResponder を呼び出さないとき、画面表示時の First Responder -> Window だった
- acceptsFirstResponder は、タブキーなどでコントロールのフォーカスを移す際に、フォーカスが移せるかどうかの判定に使用されるものっぽい (調査不足)
- マウスクリックイベントのレスポンダチェーンは NSWindow#firstResponder から始まるのではなく、クリックイベントの発生した座標に存在する View から始まるのではないかと思うが、詳細不明 (調査不足)