UIMenuBuilder - some menus refuse customization

I'm trying to customize the menu in my iPad app for iOS 26, since most of the default menu options aren't relevant to my app. Fortunately, I already have the code for this from my Mac Catalyst implementation. However, the functionality to replace menus with new sets of options has no effect on some menus on iOS. For example, updating the Format menu works fine on Mac Catalyst and iOS 26:

override func buildMenu(with builder: UIMenuBuilder) {
	super.buildMenu(with: builder)

	let transposeUpItem = UIKeyCommand(title: NSLocalizedString("TRANSPOSE_UP"), image: nil, action: #selector(TransposeToolbar.transposeUp(_:)), input: "=", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let transposeDownItem = UIKeyCommand(title: NSLocalizedString("TRANSPOSE_DOWN"), image: nil, action: #selector(TransposeToolbar.transposeDown(_:)), input: "-", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let transposeGroup = UIMenu(title: "", image: nil, identifier: UIMenu.Identifier(rawValue: "transposeGroup"), options: UIMenu.Options.displayInline, children: [transposeUpItem, transposeDownItem])

	let boldItem = UIKeyCommand(title: NSLocalizedString("BOLD"), image: nil, action: #selector(FormatToolbar.bold), input: "b", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let italicItem = UIKeyCommand(title: NSLocalizedString("ITALIC"), image: nil, action: #selector(FormatToolbar.italic), input: "i", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let underlineItem = UIKeyCommand(title: NSLocalizedString("UNDERLINE"), image: nil, action: #selector(FormatToolbar.underline), input: "u", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let colorItem = UIKeyCommand(title: NSLocalizedString("COLOR"), image: nil, action: #selector(FormatToolbar.repeatColor), input: "c", modifierFlags: [.command, .alternate], propertyList: nil, alternates: [])
	let clearItem = UIKeyCommand(title: NSLocalizedString("CLEAR_FORMATTING"), image: nil, action: #selector(FormatToolbar.clear), input: "x", modifierFlags: [.command, .alternate], propertyList: nil, alternates: [])
	let formatGroup = UIMenu(title: "", image: nil, identifier: UIMenu.Identifier(rawValue: "formatGroup"), options: UIMenu.Options.displayInline, children: [boldItem, italicItem, underlineItem, colorItem, clearItem])

	let markerItem = UIKeyCommand(title: NSLocalizedString("ADD_A_MARKER"), image: nil, action: #selector(FormatToolbar.marker), input: "m", modifierFlags: [.command, .alternate], propertyList: nil, alternates: [])
	let markerGroup = UIMenu(title: "", image: nil, identifier: UIMenu.Identifier(rawValue: "markerGroup"), options: UIMenu.Options.displayInline, children: [markerItem])

	var formatMenu = builder.menu(for: .format)
	var formatItems = [transposeGroup, formatGroup, markerGroup]
	formatMenu = formatMenu?.replacingChildren(formatItems)
	if let formatMenu = formatMenu {
		builder.replace(menu: .format, with: formatMenu)
	}
}

The same approach for the View menu works fine on Mac Catalyst, but has no effect on iOS 26. I see the same View menu options as before:

override func buildMenu(with builder: UIMenuBuilder) {
	super.buildMenu(with: builder)

	let helpItem = UIKeyCommand(title: NSLocalizedString("HELP"), image: nil, action: #selector(utilityHelp), input: "1", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let actionItemsItem = UIKeyCommand(title: NSLocalizedString("ACTION_ITEMS"), image: nil, action: #selector(utilityActionItems), input: "2", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let remoteControlItem = UIKeyCommand(title: NSLocalizedString("APP_CONTROL_STATUS"), image: nil, action: #selector(utilityRemoteControl), input: "3", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let midiItem = UIKeyCommand(title: NSLocalizedString("MIDI_STATUS"), image: nil, action: #selector(utilityMidi), input: "4", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let linkingItem = UIKeyCommand(title: NSLocalizedString("LIVE_SHARING_STATUS"), image: nil, action: #selector(utilityLinking), input: "5", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let syncItem = UIKeyCommand(title: NSLocalizedString("SYNCHRONIZATION"), image: nil, action: #selector(utilitySync), input: "6", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let settingsItem = UIKeyCommand(title: NSLocalizedString("SETTINGS"), image: nil, action: #selector(utilitySettings), input: "7", modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let utilityGroup = UIMenu(title: "", image: nil, identifier: UIMenu.Identifier(rawValue: "utilityGroup"), options: UIMenu.Options.displayInline, children: [helpItem, actionItemsItem, remoteControlItem, midiItem, linkingItem, syncItem, settingsItem])

	let backItem = UIKeyCommand(title: NSLocalizedString("BUTTON_BACK"), image: nil, action: #selector(navigateBack), input: UIKeyCommand.inputLeftArrow, modifierFlags: UIKeyModifierFlags.command, propertyList: nil, alternates: [])
	let mainMenuItem = UIKeyCommand(title: NSLocalizedString("MAIN_MENU"), image: nil, action: #selector(navigateMain), input: UIKeyCommand.inputLeftArrow, modifierFlags: [.command, .alternate], propertyList: nil, alternates: [])
	let navigateGroup = UIMenu(title: "", image: nil, identifier: UIMenu.Identifier(rawValue: "navigateGroup"), options: UIMenu.Options.displayInline, children: [backItem, mainMenuItem])

	var viewMenu = builder.menu(for: .view)
	var viewItems = [utilityGroup, navigateGroup]
	viewMenu = viewMenu?.replacingChildren(viewItems)
	if let viewMenu = viewMenu {
		builder.replace(menu: .view, with: viewMenu)
	}
}

I tried a couple other approaches, but they also did nothing:

	var viewMenu = builder.menu(for: .view)
	var viewItems = [utilityGroup, navigateGroup]
	builder.replaceChildren(ofMenu: .view, from: { oldChildren in
		viewItems
	})

Also:

	viewMenu = UIMenu(title: NSLocalizedString("VIEW"), image: nil, identifier: .view, options: UIMenu.Options.displayInline, children: viewItems)
	if let viewMenu = viewMenu {
		builder.replace(menu: .view, with: viewMenu)
	}

Does anyone know how to make this work for the View menu?

It's frustrating that Apple added all these default options, without an easy way to remove options that aren't relevant.

Answered by arlomedia in 856594022

Thanks -- your code worked for me, so I was able to work backwards from there to find the problem. My View menu contains an item that opens my app's Settings window. But the default Application menu opens the iOS Settings app, which isn't very useful to users, so I had also overridden that menu to open my app's Settings window. It turns out a menu won't display if it contains an action that's already in another menu.

And now I see a warning about this in the Xcode console, which I hadn't noticed before. 🙄

It was still working in Mac Catalyst because the default Application menu there is more useful and I wasn't overriding it, so I wasn't creating a duplicate.

I simply made a second function that opens my app's Settings window, and used one in the Application menu and the other in the View menu, and now it's working.

I had no trouble replacing the View menu with code similar to the following:

override func buildMenu(with builder: any UIMenuBuilder) {
    super.buildMenu(with: builder)

    if builder.system == .main {
        let cmd = UIKeyCommand(title: "Hello", image:nil, action:#selector(doStuff), input: "1", modifierFlags: .command)
        let menu = UIMenu(image: nil, options: .displayInline, children: [cmd])

        if let view = builder.menu(for: .view) {
            let newView = view.replacingChildren([menu])
            builder.replace(menu: .view, with: newView)
        }
    }
}

With that code the View menu only shows the one menu item I created.

Tested with Xcode 26 beta 7 running on a simulated iPad running iPadOS 26 beta 6.

Accepted Answer

Thanks -- your code worked for me, so I was able to work backwards from there to find the problem. My View menu contains an item that opens my app's Settings window. But the default Application menu opens the iOS Settings app, which isn't very useful to users, so I had also overridden that menu to open my app's Settings window. It turns out a menu won't display if it contains an action that's already in another menu.

And now I see a warning about this in the Xcode console, which I hadn't noticed before. 🙄

It was still working in Mac Catalyst because the default Application menu there is more useful and I wasn't overriding it, so I wasn't creating a duplicate.

I simply made a second function that opens my app's Settings window, and used one in the Application menu and the other in the View menu, and now it's working.

You may want to reconsider removing the standard iOS settings menu. It gives the user easy access to your app’s privacy and notification settings along with a few others. In my own app I have my own in-app settings screens. I simply added another menu item to the app menu so the user can either choose to go to the app’s page in the iOS Settings app or open my own in-app settings screen.

UIMenuBuilder - some menus refuse customization
 
 
Q