How does a menu item find its target on the responder chain, what do key equivalents do, and what happens for a disabled item?
Predict first
What do the menu validation and the key events do?
import AppKit
@MainActor
final class Responder: NSObject, NSMenuItemValidation {
var log: [String] = []
@objc func save(_ sender: Any?) { log.append("save") }
@objc func export(_ sender: Any?) { log.append("export") }
func validateMenuItem(_ item: NSMenuItem) -> Bool { item.action != #selector(export(_:)) }
}
@MainActor func demo() {
let r = Responder()
let quit = NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
let save = NSMenuItem(title: "Save", action: #selector(Responder.save(_:)), keyEquivalent: "s")
let exp = NSMenuItem(title: "Export", action: #selector(Responder.export(_:)), keyEquivalent: "e")
save.target = r; exp.target = r
print("quit uses Command:", quit.keyEquivalentModifierMask == .command, "| target nil (responder chain):", quit.target == nil)
let menu = NSMenu(title: "File")
[save, exp].forEach(menu.addItem)
menu.update()
print("save enabled:", save.isEnabled, "| export enabled:", exp.isEnabled)
func key(_ c: String, _ code: UInt16) -> NSEvent {
NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: .command, timestamp: 0, windowNumber: 0, context: nil, characters: c, charactersIgnoringModifiers: c, isARepeat: false, keyCode: code)!
}
print("cmd-S handled:", menu.performKeyEquivalent(with: key("s", 1)), r.log)
print("cmd-E handled:", menu.performKeyEquivalent(with: key("e", 14)), r.log)
}
MainActor.assumeIsolated { _ = NSApplication.shared; demo() }
- save enabled: true, export enabled: false; ⌘S logs save; ⌘E returns true but logs nothing new
- both enabled; both log
- neither handled
Mental model. A menu command is a note addressed “to whoever is responsible” that the window hands up its chain of command until someone says “mine”; a disabled item is a note the manager refuses to forward and also refuses to let anyone else read.
An NSMenuItem has an action selector and an optional target. With a nil target (the usual case for Quit, Save, Copy) AppKit walks the responder chain from the key window’s first responder through the window, its controller and the application to the app delegate, and the first object that responds to the selector and validates becomes the target; with an explicit target it asks only that object. Validation is part of the contract: with autoenablesItems the menu asks validateMenuItem(_:) (NSMenuItemValidation) before it shows or fires an item. The check builds a File menu with a target that rejects Export: after menu.update() Save is enabled and Export is not, ⌘S sends save, and ⌘E is handled (returns true) but does nothing: the menu consumes the key equivalent of a disabled item so it does not fall through to another handler.
A key equivalent defaults to the Command modifier (Quit with "q" reports .command); an uppercase key string did not add Shift in a separate run, so set keyEquivalentModifierMask explicitly. Design consequences: use nil targets for commands that depend on focus, make the command live where its state lives (the document controller, not a singleton), and test enablement by calling validateMenuItem or by driving performKeyEquivalent as the check does. SwiftUI’s .commands { CommandGroup … } and .keyboardShortcut build the same menu; FocusedValue and @FocusedObject take the place of the responder chain for enablement. The check needs a running NSApplication (touching NSApplication.shared); without it the items stayed disabled, which looks like a validation bug. Not run: a full window hierarchy and the Touch Bar.
The disabled item swallows its key equivalent without firing.
Common traps
- Setting an explicit target on a singleton for every item. The command then acts regardless of focus, and the menu enables it when it should not.
- Testing menu actions without
NSApplication.sharedrunning. Items stay disabled and the test misleads.
They might follow up with
- How does SwiftUI choose the target? Through focused values and the focused scene, not a literal responder chain.
- Where do I handle ⌘, (Settings)? A
Settingsscene in SwiftUI or a menu item in AppKit.
Related questions
- 20.3.2 What does a Mac app need that an iOS app does not: the menu bar, windows, the App Sandbox, Hardened Runtime…
- 20.3.1 Mac Catalyst, native AppKit, SwiftUI for macOS, or an iPad app on the Mac: how do you choose, and what…
← What is the difference between a stored path and a bookmark, and how… What does a Mac Catalyst build compile against, and how do you add… →