Adding menus and shortcuts to the menu bar and user interface
At a glance
| Item |
Summary |
| Purpose |
Builds Mac Catalyst menu-bar commands, keyboard shortcuts, contextual menus, and a native toolbar around a split-view editor. |
| App architecture |
AppDelegate owns main-menu construction/validation; split-view controllers own item state and responder actions; Catalyst toolbar behavior is isolated in an extension. |
| Main patterns |
Menu builder/factory, responder-chain commands, weak detail delegate, platform adapter extension, and preferences observer. |
| Project style |
Storyboard UIKit target with primary/detail controllers and separate table/toolbar extension files. |
Project structure
Menus/
├── AppDelegate.swift
├── SceneDelegate.swift
├── MenuController.swift
├── PrimaryViewController.swift
├── PrimaryViewController+Table.swift
├── PrimaryViewController+Toolbar.swift
├── DetailViewController.swift
├── TableItem.swift
└── Base.lproj/Main.storyboard
Structure observations
MenuController constructs command values; command targets remain on responders such as AppDelegate and the primary controller.
- Primary controller extensions separate table mechanics and Catalyst-only toolbar integration without splitting state ownership.
- Detail-to-primary mutations travel through a weak protocol delegate.
Overall architecture
flowchart LR
App["AppDelegate"] --> Builder["UIMenuBuilder"]
App --> Factory["MenuController"]
Factory --> Menus["UIMenu and UIKeyCommand tree"]
Menus -. selectors via responder chain .-> Primary["PrimaryViewController"]
Menus -. app-wide selectors .-> App
Scene["SceneDelegate"] --> Split["UISplitViewController"]
Split --> Primary
Split --> Detail["DetailViewController"]
Primary -. weak DetailItemDelegate .-> Detail
Primary --> Toolbar["Catalyst NSToolbar extension"]
Reference code
Menus/AppDelegate.swift:37 — application-level menu construction delegates the command tree to MenuController and keeps validation state in AppDelegate.
override func buildMenu(with builder: UIMenuBuilder) {
if builder.system == .main {
menuController = MenuController(with: builder)
fontMenuStyleStates.insert(MenuController.FontStyle.plain.rawValue)
}
}
override func validate(_ command: UICommand) {
if let styles = command.propertyList as? [String: String],
let style = styles[MenuController.CommandPListKeys.StylesIdentifierKey] {
command.state = fontMenuStyleStates.contains(style) ? .on : .off
}
}
Interpretation
Menu definitions and effects are intentionally separated. The builder installs commands with selectors and property-list payloads; UIKit resolves selectors through AppDelegate or the current responder, while validation projects current state back onto commands.
Ownership and state
classDiagram
AppDelegate *-- MenuController : retains built menu helper
AppDelegate *-- Set~String~ : owns style state
UISplitViewController *-- PrimaryViewController : storyboard child
UISplitViewController *-- DetailViewController : storyboard child
PrimaryViewController *-- AnyModelItem : stores table items
PrimaryViewController o-- DetailViewController : lazy lookup
DetailViewController o-- DetailItemDelegate : weak
DetailViewController *-- NSKeyValueObservation : retains preferences token
PrimaryViewController o-- NSToolbarItem : remembers installed items
Ownership evidence
Menus/PrimaryViewController.swift:16 — the primary owns item state and lazily resolves the storyboard-owned detail, then installs itself as weak delegate.
var tableItems = [AnyModelItem]()
lazy var detailViewController: DetailViewController = {
var result = DetailViewController()
if let navigation = splitViewController?.viewControllers[1] as? UINavigationController,
let detail = navigation.topViewController as? DetailViewController {
result = detail
result.detailItemDelegate = self
}
return result
}()
| Owner |
Object or state |
Relationship |
Mutation authority |
| AppDelegate |
Menu helper and checked font styles |
Creates/retains |
Menu build, actions, validation |
| Storyboard split controller |
Primary/detail navigation stacks |
Creates/retains |
UIKit/storyboard lifecycle |
| Primary controller |
Model items, selection, formatter, Catalyst toolbar references |
Owns |
Table and command actions |
| Detail controller |
Current item and preference-observation token |
Receives item; owns observation |
Detail edits/observer callbacks |
| Detail controller |
DetailItemDelegate |
Weak reference to primary |
Primary implements mutations |
Class and protocol design
| Type or protocol |
Responsibility |
Depends on |
MenuController |
Define menu identifiers, commands, hierarchies, key equivalents, and payloads |
UIMenuBuilder and selector targets |
AppDelegate |
Build main menu, validate app-level command state, receive global actions |
UIApplicationDelegate |
PrimaryViewController |
Own item collection and responder actions |
Table, detail delegate, toolbar extension |
DetailItemDelegate |
Cut/copy/paste/delete and persist detail changes |
Primary controller |
DetailViewController |
Present/edit one item, contextual menu, print, preferences |
Weak delegate, UserDefaults, UIContextMenuInteractionDelegate |
AnyModelItem |
Represent date-or-text rows and sharing item providers |
UIActivityItemsConfigurationReading |
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
PrimaryViewController.selectRow |
private |
Selection mechanics stay inside the primary file/type. |
Prevent external code from bypassing table coordination. |
DetailViewController.detailItemDelegate |
implicit internal weak |
Target code may wire it, without retaining primary. |
Storyboard collaborators need assignment; weak avoids a cycle. |
| Menu factories and identifiers |
implicit internal |
AppDelegate and primary can share them across files. |
Command construction spans multiple responders. |
| Catalyst toolbar extension |
compile-time #if targetEnvironment(macCatalyst) |
Toolbar symbols do not exist in non-Catalyst builds. |
Platform boundary is stronger than runtime branching. |
| Most state |
implicit internal |
Same-target extensions can collaborate. |
Extension-based organization trades narrow lexical privacy for file separation. |
Reference code
Menus/DetailViewController.swift:20 — the app-defined collaboration is target-local, and the stored delegate is weak.
protocol DetailItemDelegate: NSObjectProtocol {
func performCutAction()
func performCopyAction()
func performPasteAction()
func performDeleteAction()
func didUpdateItem(_ item: AnyModelItem)
}
weak var detailItemDelegate: DetailItemDelegate?
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Menu hierarchy and key-command factories |
MenuController |
Pure command construction grouped away from effects. |
| App-wide action/validation state |
AppDelegate |
Main menu system asks application and delegate. |
| Row CRUD and responder validation |
Primary controller |
Owns item collection and selection. |
| Detail rendering, contextual actions, printing |
Detail controller |
Operations act on current detail item/view. |
| Native toolbar construction/actions |
PrimaryViewController+Toolbar.swift |
Catalyst-specific adapter around primary commands. |
| Split collapse policy |
SceneDelegate |
Scene-level container behavior. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Menu builder/factory |
Menus/MenuController.swift:83 |
Centralizes declarative menu topology and command payloads. |
| Responder chain |
Menus/MenuController.swift:139 |
Selectors target the active capable controller without hard references from the menu tree. |
| Command validation |
Menus/AppDelegate.swift:54 |
Keeps checked/disabled state synchronized with app state. |
| Weak delegate |
Menus/DetailViewController.swift:20 |
Returns detail commands to the collection owner without a retain cycle. |
| Platform adapter extension |
Menus/PrimaryViewController+Toolbar.swift:10 |
Keeps Catalyst-only NSToolbar code out of cross-platform table logic. |
| Observer |
Menus/DetailViewController.swift:58 |
Projects preference changes into the visible detail automatically. |
Naming conventions
MenuController is a construction helper, while action methods end in Action and match selectors.
- Nested enums (
Cities, Tools, FontStyle, Arrows) model finite menu groups; CommandPListKeys names payload keys.
+Table and +Toolbar filenames identify extensions of the same owner.
- Toolbar identifiers use
...ID; Boolean availability is expressed through responder validation rather than stored is... flags.
Architecture takeaways
- Separate menu topology from command effects, then let selectors and responder routing connect them.
- Validate commands from the same owner that holds their state.
- Compile platform-specific integrations out of unsupported targets and keep them in focused extensions.
- Use weak delegates when a detail screen edits data owned by its primary/list screen.
Source map
| Source file |
Relevant symbols |
Menus/AppDelegate.swift |
Main-menu build, actions, and validation |
Menus/MenuController.swift |
Menu/command factories and identifiers |
Menus/PrimaryViewController.swift |
Item state and responder actions |
Menus/PrimaryViewController+Table.swift |
Table data-source/delegate mechanics |
Menus/PrimaryViewController+Toolbar.swift |
Catalyst toolbar adapter |
Menus/DetailViewController.swift |
Weak delegate, context menu, printing, preferences |
Menus/SceneDelegate.swift |
Split-view configuration/collapse policy |