"Arrow keys worked every time. The comma and period chords, bound only as menu shortcuts, read as missing."
How a Key Finds Its Handler
On the Mac, a key press travels a defined path. First, NSEvent local monitors installed by your app see it. Then the window gives views a chance at key equivalents through performKeyEquivalent(with:), walking down the view tree, before the menu bar tries to match a menu item's shortcut. Only after that does a plain key reach the first responder as keyDown(with:), and unhandled actions travel up the responder chain: the view, its superviews, the window, the window controller, the application, and the app delegate. A menu item with a nil target sends its action up that chain from the first responder.
Knowing that order is what fixes the family's keyboard bugs, because each one was a key arriving at a different step than expected.
Three Fixes, Three Steps of the Path
- Menu shortcuts are not a reliable daily input path. A video player bound frame-step and loop chords (
,.[]with no modifiers) only as SwiftUI menu key equivalents. They fired inconsistently and gave no feedback, while arrow keys handled by a localNSEventmonitor worked every time. The chords moved to the monitor, the menu items stayed as discoverable labels showing the same keys, and the monitor consumes the event so nothing double-fires. - Standard editing keys can be missing in a field. Text fields inside an
NSAlertaccessory in a file workbench got no ⌘V, ⌘C, ⌘X or ⌘A, because the app's Edit menu routed those to custom actions. Because the window offers key equivalents to views before the menu, anNSTextFieldsubclass that handles them inperformKeyEquivalent(with:)works regardless of menu state. - A library class you cannot override. A terminal view from a third-party library declared
keyDown(with:)aspublic, notopen, so subclassing to add a shortcut failed to compile. A local monitor that checks whether that view is the window's first responder handles the chord before the view ever sees it.
Two Quieter Traps
Selector names are a shared namespace. NSResponder already defines actions such as moveUp(_:) and moveDown(_:); an app action with the same name becomes an accidental override that the text system and arrow keys will call. Input methods change what "typing" means. With a Korean input source active, synthesized ASCII keystrokes arrive as Hangul, and composition happens as marked text before a character is committed. Keyboard behaviour in an editor or terminal is not verified until it has been exercised with an input method in the installed app.