Adopting menus and UIActions in your user interface
At a glance
| Item |
Summary |
| Purpose |
Demonstrates menus and UIAction on buttons, bar items, segmented controls, custom controls, deferred elements, and visible-menu updates. |
| App architecture |
A root custom menu control navigates to independent programmatic demo controllers; each screen owns one menu API variation. |
| Main patterns |
Action-as-value, example catalog, custom control, deferred provider, and lifecycle-scoped timer. |
| Project style |
One UIKit target with one controller per demo and no separate domain model. |
Project structure
ControlMenus/
├── ViewController.swift
├── CustomMenuControl.swift
├── ButtonMenuDemoViewController.swift
├── BarButtonItemDemoViewController.swift
├── SegmentedControlDemoViewController.swift
├── DeferredMenuElementDemoViewController.swift
├── UpdateMenuViewController.swift
├── AppDelegate.swift
└── SceneDelegate.swift
Structure observations
UIAction values carry titles, state, and closures directly; there is no command-model layer.
CustomMenuControl is both reusable navigation control and the most complete custom-menu example.
- Asynchronous and live-update lifecycles stay in their dedicated demo controllers.
Overall architecture
flowchart LR
Root["ViewController"] --> Selector["CustomMenuControl"]
Selector --> Actions["UIAction values"]
Actions --> Nav["UINavigationController"]
Nav --> Button["Button demo"]
Nav --> Bar["Bar-button demo"]
Nav --> Segment["Segmented-control demo"]
Nav --> Deferred["Deferred-element demo"]
Nav --> Update["Visible-menu update demo"]
Selector --> Menu["Computed UIMenu of proxy actions"]
Reference code
ControlMenus/ViewController.swift:15 — the root expresses navigation as action values installed on one custom menu control.
let customMenuControl = CustomMenuControl(
frame: .zero,
primaryAction: UIAction(title: "Demo Selector") { _ in })
customMenuControl.showSelectedItem = false
customMenuControl.items = [
UIAction(title: "Button Demo") { [unowned self] _ in
self.navigationController?.pushViewController(ButtonMenuDemoViewController(), animated: true)
},
UIAction(title: "Deferred Element Demo") { [unowned self] _ in
self.navigationController?.pushViewController(DeferredMenuElementDemoViewController(), animated: true)
}
]
Interpretation
The sample treats actions as composable values: the same object can describe a menu item and execute navigation or UI effects. Demo screens remain intentionally independent so their UIKit configurations are easy to compare.
Ownership and state
classDiagram
ViewController *-- CustomMenuControl : creates and adds
CustomMenuControl *-- Array~UIAction~ : stores items
CustomMenuControl *-- UILabel : lazy private child
CustomMenuControl --> UIMenu : computes
UIMenu *-- UIAction : children
UpdateMenuViewController *-- UIContextMenuInteraction : stores
UpdateMenuViewController *-- Timer : owns while visible
DeferredMenuElement --> CompletionClosure : completes asynchronously
Ownership evidence
ControlMenus/CustomMenuControl.swift:19 — the control owns source actions and selection state; changing them refreshes or disables its menu.
var items: [UIAction] = [] {
didSet {
if items.isEmpty {
contextMenuInteraction?.dismissMenu()
isContextMenuInteractionEnabled = false
} else {
updateMenuIfVisible()
isContextMenuInteractionEnabled = true
}
}
}
var selectedIndex: Int = -1 {
didSet { updateMenuIfVisible() }
}
| Owner |
Object or state |
Relationship |
Mutation authority |
| Root controller/view hierarchy |
Selector control |
Creates/adds; hierarchy retains |
Root setup |
| Custom control |
Actions, selected index, display policy, private label |
Strongly owns |
Property observers and proxy actions |
| Each demo controller |
Its controls/menu values |
Creates/adds |
Local action closures |
| Update demo |
Context-menu interaction and timer |
Retains; invalidates timer on menu end |
Framework lifecycle callbacks |
| Deferred element |
Async completion contract |
UIKit retains provider closure |
Provider must call completion |
Class and protocol design
| Type |
Responsibility |
Depends on |
CustomMenuControl |
Turn source actions into selectable proxy actions, update visible menus, animate presentation |
UIControl context-menu behavior |
Root ViewController |
Build the demo selector and push screens |
Navigation controller |
| Button/bar/segment demo controllers |
Show built-in menu attachment points |
UIKit controls |
| Deferred demo |
Append async menu children |
UIDeferredMenuElement |
Update demo / UIContextMenuInteractionDelegate |
Adapt rich/compact presentation and mutate visible actions over time |
Interaction/timer |
No app-defined protocol exists; UIAction closures and UIKit delegate methods provide the collaboration boundaries.
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
CustomMenuControl.titleLabel |
private |
Callers configure the control through actions/state, not label internals. |
Preserve custom-control rendering authority. |
| Control items and selection |
implicit internal |
Root controller can configure them across files. |
Intended target-level API of the custom control. |
| Demo controllers and helpers |
implicit internal |
Available within the app target only. |
Catalog app, not framework. |
[unowned self] action captures |
nonowning capture |
Actions do not retain their owning controller. |
Avoid cycles while assuming actions do not outlive screens. |
Reference code
ControlMenus/CustomMenuControl.swift:54 — the private lazy label is created only through the control’s own rendering path.
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.textColor = tintColor
label.highlightedTextColor = .secondaryLabel
addSubview(label)
return label
}()
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Catalog navigation |
Root controller |
Knows all demonstration screens. |
| Selection/proxy action/menu update |
Custom control |
Defines the reusable control contract. |
| Built-in control configurations |
Corresponding demo controller |
Keeps each UIKit variation isolated. |
| Async child production |
Deferred demo |
Owns completion timing. |
| Live action state/visibility updates |
Update demo |
Owns interaction and timer lifecycle. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Action-as-value |
ControlMenus/ViewController.swift:17 |
Co-locates menu metadata and behavior; closures require capture discipline. |
| Custom control |
ControlMenus/CustomMenuControl.swift:13 |
Packages menu selection, appearance, and target-action forwarding. |
| Proxy action |
ControlMenus/CustomMenuControl.swift:97 |
Adds selection state while forwarding the original command. |
| Deferred provider |
ControlMenus/DeferredMenuElementDemoViewController.swift:21 |
Loads dynamic children only when presentation needs them. |
| Lifecycle timer |
ControlMenus/UpdateMenuViewController.swift:78 |
Updates only while the menu is visible, then invalidates. |
Naming conventions
- Demo types end in
DemoViewController; names identify the UIKit API under study.
items, selectedIndex, and showSelectedItem form the custom control’s small state API.
- Factory/forwarding methods use
proxyAction, menu, and updateMenuIfVisible.
Architecture takeaways
- Model menu commands as values when metadata and effect naturally travel together.
- Keep a custom control’s subviews private and expose semantic items/selection instead.
- Scope timers and deferred work to menu presentation lifecycle.
- Treat catalog screens as independent examples, not a forced application-layer hierarchy.
Source map
| Source file |
Relevant symbols |
ControlMenus/ViewController.swift |
Action-driven demo navigation |
ControlMenus/CustomMenuControl.swift |
Custom control state, proxy actions, visible updates |
ControlMenus/ButtonMenuDemoViewController.swift |
UIButton menu/action variations |
ControlMenus/DeferredMenuElementDemoViewController.swift |
Async menu elements |
ControlMenus/UpdateMenuViewController.swift |
Presentation adaptation and live updates |