How are UIMenu, UIAction and deferred elements structured, and where are menus used?

7.11.3 · L2 · Interview

Predict first

What do flattening, replacing children and a deferred element give?

import UIKit

let share = UIAction(title: "Share") { _ in }
let rename = UIAction(title: "Rename") { _ in }
let del = UIAction(title: "Delete", attributes: .destructive) { _ in }
let sort = UIMenu(title: "Sort", options: .displayInline, children: [UIAction(title: "Name") { _ in }, UIAction(title: "Date") { _ in }])
let menu = UIMenu(children: [share, rename, sort, del])
func leaves(_ e: UIMenuElement) -> [String] {
    if let m = e as? UIMenu { return m.children.flatMap(leaves) }
    return [e.title]
}
print(menu.children.map(\.title))
print(leaves(menu))
let smaller = menu.replacingChildren([rename])
print(smaller.children.count, menu.children.count)
var loaded = 0
let deferred = UIDeferredMenuElement { done in loaded += 1; done([UIAction(title: "Late") { _ in }]) }
let m3 = UIMenu(children: [deferred])
print(m3.children.count, loaded)
  • ["Share", "Rename", "Sort", "Delete"] / ["Share", "Rename", "Name", "Date", "Delete"] / 1 4 / 1 0
  • ["Share", "Rename", "Sort", "Delete"] / … / 1 1 / 1 1
  • ["Share", "Rename", "Name", "Date", "Delete"] first line

Mental model. A menu is a printed card, not a living object: to change a dish you print a new card, and some lines are marked "ask the waiter" and only get filled in when the card is opened.

A menu is a tree of UIMenuElement values: UIAction leaves (title, image, state, attributes such as .destructive and .disabled, and a handler), UIMenu nodes, and UIDeferredMenuElement placeholders. They are immutable values: replacingChildren(_:) returns a new menu and leaves the original alone (the check shows 1 child versus 4), so to change a menu you assign a rebuilt one. A submenu with .displayInline is shown as a separated group instead of a nested level, which is how Sort and Delete groups are built; flattening the tree (the leaves function) is how you test or search it. Identifiers (UIAction.Identifier) let you find and replace actions in a menu you did not build.

The same menu object plugs into many places: button.menu with showsMenuAsPrimaryAction = true (a tap shows it, long press otherwise), UIBarButtonItem(title:menu:), UIContextMenuInteraction for long-press context menus with previews, and the main menu on iPad and Catalyst through UIMenuBuilder (Q255). A UIDeferredMenuElement defers building its items: its provider had run zero times after the menu was created, and it runs when the menu is about to be shown, with a placeholder while loading; use .uncached when the content must be rebuilt on every presentation. Handlers run on the main thread; keep them short and do not capture the controller strongly if the menu is stored on a long-lived object.

Menus are value trees: replacingChildren returns a copy and a deferred element has not loaded yet.

Common traps

  • Mutating a menu in place. There is nothing to mutate; rebuild and reassign.
  • Expecting deferred content to be loaded at creation. It loads when the menu is presented.

They might follow up with

  • How do I show a checkmark? Set the action's state to .on and rebuild the menu when selection changes.
  • How is this different from SwiftUI? Menu and contextMenu play the same roles declaratively.

Related questions

  • 7.8.1 How do you support hardware keyboards, shortcuts, menus and the pointer on iPad?
  • 7.11.2 Which UIKit patterns keep a view controller alive after it is dismissed, and how do you break each one?

← Which UIKit patterns keep a view controller alive after it is…