Skip to content
macOSDeep Dive Published Updated 6 min readViews unavailable

NSMenuItem Validation on macOS: Enable Commands From Real State

Keep AppKit menus truthful with responder-chain validation, inexpensive menu updates, clear command ownership, and tests for changing selection and document state.

An enabled menu item tells the user that its action is currently available. If “Delete” stays enabled with no selection, “Save” is enabled for a read-only document, or “Next” remains enabled on the final page, the menu is not merely untidy: it advertises a command that does not match application state. AppKit menu validation provides a way to update that state as menus are prepared, but it works best when command ownership, responder routing, and validation cost are designed together.

NSMenu can automatically enable and disable items according to menu validation rules. An object that is the target of an item can implement NSMenuItemValidation and decide whether that command is enabled. In responder-chain menus, target resolution and validation depend on the current key window and first responder. A menu should therefore reflect the active document or selection, not a global controller that happens to be alive.

Validate based on the command target

The validator receives the menu item and can identify the operation by its action or tag. Keep the method deterministic and cheap: inspect already-available state, do not fetch remote data, wait on a lock, open a modal dialog, or mutate the model. Validation may occur repeatedly as menus are updated. It should answer “is this operation currently available?” and leave execution to the action method.

import AppKit

@MainActor
final class DocumentCommands: NSObject, NSMenuItemValidation {
    weak var document: EditorDocument?

    func validateMenuItem(_ item: NSMenuItem) -> Bool {
        switch item.action {
        case #selector(copy(_:)):
            return document?.selection.isEmpty == false
        case #selector(save(_:)):
            return document?.isEditable == true && document?.isDocumentEdited == true
        default:
            return true
        }
    }

    @objc func copy(_ sender: Any?) { document?.copySelection() }
    @objc func save(_ sender: Any?) { document?.saveIfNeeded() }
}

This example assumes EditorDocument exposes inexpensive state on the main actor. In an actual app, if save should remain enabled for “Save As” or other behavior, model those commands separately rather than applying one broad condition to every save-related item. Validation is command-specific policy.

Automatic enabling versus explicit control

NSMenu auto-enables items by default. This behavior asks the menu item’s target or responder chain whether an action is valid. If an app disables automatic enabling, it owns the explicit enabled state and must update it whenever relevant model state changes. Mixing both strategies can create menus that appear correct only after one path has run.

Keep automatic enabling on when target-action and responder validation express the command semantics cleanly. Use explicit menu updates for a dynamically constructed menu whose items have independent state that the responder chain cannot represent. If an item is disabled because an operation is temporarily running, expose that state intentionally; do not leave it enabled and silently ignore the action.

Menu validation is distinct from menu construction. NSMenuDelegate callbacks such as menuNeedsUpdate can populate or reorder items just before display. Build the menu from a fast local snapshot and validate the actions after the item set is known. Do not do expensive model queries in both the delegate and validator. If a remote capability check is required, cache its last known result with a timestamp and show a clear pending state rather than blocking menu opening.

Responder-chain target resolution

The responder chain lets a command be handled by the first object that understands it, often the first responder, view controller, window controller, document, or application. This supports menus that work with the currently focused editor without wiring every menu item to one global controller. It also means a responder-chain action can be unavailable when focus moves to a text field, sheet, inspector, or another document.

Test validation from each relevant key-window and focus context. A “Select All” action may belong to a text view when editing text and to a document controller when the canvas is active. “Delete” should be routed to the current selection owner. Avoid global validator state that uses NSApp.keyWindow to infer the target during an action; the target itself should own the state it validates.

If an action is available only in a specific mode, reflect that in validation and enforce the same precondition in the action handler. A menu can become stale between display and activation if state changes while it is open. Never use validation as the only protection around a destructive mutation. Re-check the model at action time and handle a now-invalid command as a safe no-op or clear user-visible conflict.

Titles, state, and alternate commands

Enabled state and menu title are separate decisions. A checkmark should correspond to an actual model setting; a title that toggles between “Show” and “Hide” should be derived from current visibility. If changing a title dynamically, preserve keyboard equivalents, localization, accessibility labels, and menu item identity. Do not infer state from the current title string because localized or customized text can differ.

When there are multiple selections or partial state, decide whether a command applies to all selected objects, only eligible objects, or none. A single Boolean cannot describe the full semantics; if only some records are writable, either disable the command with an explanatory reason elsewhere or make the action explicitly operate on eligible objects and report the result. Do not silently skip items in a way the user cannot discover.

Use menu item tags sparingly for stable command identity in programmatically generated menus. Prefer selector/action identity when it is unambiguous, and avoid comparing display titles. For repeated commands across documents, let each document or its command controller provide validation for its own current state.

Keep validation pure and inexpensive

Validation should not change selection, register undo, write to disk, or trigger notifications. A menu update should not have side effects simply because the user opened a menu. Cache derived state if computing it is expensive, and make the cached state update when the underlying model changes. If the validation logic needs to await an actor or service, move that work earlier and publish an immutable command-state value for the menu to inspect synchronously.

Do not synchronously scan thousands of selected objects every time the menu opens. Maintain aggregate capability state as the selection changes, or validate based on a bounded visible subset when the command has corresponding semantics. Profile actual menu-open latency with realistic documents. The best validation function is fast enough that users do not perceive menu opening as a state refresh operation.

Verification and accessibility

Create tests for empty selection, one selected item, mixed capabilities, read-only documents, unsaved edits, multiple windows, text-field focus, modal sheets, and state changes while a menu is open. Assert both enabled state and action safety. For menu delegate updates, verify that opening and reopening the menu does not duplicate items or stale dynamic entries.

VoiceOver users need meaningful labels and state, not only an enabled/disabled color cue. Ensure menu titles remain clear, checkmarks represent actual settings, and disabled commands have a discoverable alternative when the reason is important. Standard menu conventions such as Undo, Save, and Select All should remain consistent with AppKit expectations.

A menu is trustworthy when every visible command reflects the active state, every action revalidates its preconditions, and menu preparation is fast and side-effect-free. Use responder-chain validation for context-sensitive operations, keep dynamic construction separate from validation, and test the focus paths users actually take.

Related:

Sources:

Comments