Is there a tech note for menuBuilder?

I'm trying to piece together how I should reason about multiple windows, activating menus and items.

For instance, I have an email command. If a document is open, it emails just that document. If the collection view holding that document is open, it generates an attachment representing the collection. (this happens with buttons right now but this really feels like a menu item) How do I tell which window is active (document/collection) in order to have the right menu item available.

If the user is adding a document I don't want the "new" command open, I only want one editing view. I saw sample code to include or remove commands but not disable them. I feel like there's a whole conceptual layer I want to understand with the interplay with scenes but don't know where to look for documentation. I searched here but there's hardly any threads on this.

TIA

Answered by RickMaddy in 852982022

My menu code is all in Objective-C too (old app still going). I've been using UIMenuBuilder since iOS 14 in support of macOS via Mac Catalyst. My proposed solution also works now on the iPad in iOS 26.

Your case is slightly more complicated than mine. I support multiple scenes but all of the scenes are the same class with the same root view controller class.

First, design your overall app menu taking into account the needs of general app-level menu items and items specific to each of your scenes' root view controllers. In the end you will have common menu items used by all scenes, you will have items specific to each scene, and you will have items that work even if there are no open scenes.

Once you know all of the possible menu items, you will use UIMenuBuilder in the app delegate to build the entire possible menu structure.

Then there will be validation code in the app delegate and each of your possible root view controllers. Each root view controller will only validate the items it can handle. For those it can't it calls super. And the app delegate handles what it can and for those it can't it disables them.

With that general approach, if Scene A is in focus, then it validates its own specific menu item and punts to the app delegate for the rest. The app delegate will handle a few and disable the rest. This means that while Scene A is in focus, any menu items specific to Scene B will be disabled (in the app delegate by default).

Here's some example skeleton code:

In AppDelegate.m:

- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder {
    [super buildMenuWithBuilder:builder];

    if (builder.system != UIMenuSystem.mainSystem) {
        return;
    }

    // Remove unwanted standard File menu:
    [builder removeMenuForIdentifier:UIMenuOpenRecent];
    [builder removeMenuForIdentifier:UIMenuOpen];
    [builder removeMenuForIdentifier:UIMenuDocument];

    // Add some new File menu items
    UICommand *item1 = [UIKeyCommand commandWithTitle:@"Item 1" image:[UIImage systemImageNamed:@"some.symbol"] action:@selector(item1MenuAction:) input:@"I" modifierFlags:UIKeyModifierShift | UIKeyModifierCommand propertyList:nil];
    UICommand *item2 = [UIKeyCommand commandWithTitle:@"Item 2" image:[UIImage systemImageNamed:@"other.symbol"] action:@selector(item2MenuAction:) input:@"J" modifierFlags:UIKeyModifierShift | UIKeyModifierCommand propertyList:nil];

    UIMenu *extraItems = [UIMenu menuWithTitle:@""  image: nil identifier:nil options:UIMenuOptionsDisplayInline children:@[ item1, item2 ]]; // NO_I18N

[builder insertSiblingMenu:extraItems afterMenuForIdentifier:UIMenuClose];

    // Build everything else as needed
}

- (void)validateCommand:(UICommand *)command {
    // Handle App level items here
    if (
        command.action == @selector(someMenuAction:) ||
        command.action == @selector(otherMenuAction:) ||
        NO) {
        command.attributes &= ~UIMenuElementAttributesDisabled; // Always on
    } else if (
        command.action == @selector(anotherMenuAction:) ||
        NO) {
        if (someCondition) {
            command.attributes &= ~UIMenuElementAttributesDisabled;
        } else {
            command.attributes |= UIMenuElementAttributesDisabled;
        }
    } else {
        [super validateCommand:command];
    }
}

Now add a validateCommand: to each of your root view controller classes. It should validate each of its own commands as needed. Call super for all other commands.

Tedious but straight forward.

There’s a sample app you can refer to. Look at the documentation for UIMenuBuilder. The overview has a link to the “Adding menus and shortcuts to the menu bar and user interface” sample app.

In short, the menu walks the responder chain to find the responder that handles the selector. This means the active window and its view hierarchy will be looked at to handle the menu. If your window’s rootViewController is where you handle the menu‘s selector then that controller should know its own document to handle.

In my own app my main root view controller class handles most menu items. This makes it easy for a given window to handle and validate the menu. The app delegate is only used to handle and validate menu items that are not window specific.

I guess what I'm trying to wrap my head around is the guidance in the video from this year where it says, don't hide menu items, or change how the menu is built from under the user, just disable items, Build the menu once in appDidFinishLaunching, but then the menus get built by each scene type?

I'm trying to go from a spit view ish one-window app, to multiple scenes, where there are different scenes and storyboards, so sure my window's root view controller can handle things, but each scene will have a different root controller, how do you build something that coalesces them all.

I guess I could just experiment but the builder looks clunky enough that I'd be annoyed building something 5 times if I got the assumptions wrong.

Thanks for the pointer, I've used that sample a lot already building context menus.

(I'm also doing it all backwards in obj.c so I have to squint real hard at the swift.)

Accepted Answer

My menu code is all in Objective-C too (old app still going). I've been using UIMenuBuilder since iOS 14 in support of macOS via Mac Catalyst. My proposed solution also works now on the iPad in iOS 26.

Your case is slightly more complicated than mine. I support multiple scenes but all of the scenes are the same class with the same root view controller class.

First, design your overall app menu taking into account the needs of general app-level menu items and items specific to each of your scenes' root view controllers. In the end you will have common menu items used by all scenes, you will have items specific to each scene, and you will have items that work even if there are no open scenes.

Once you know all of the possible menu items, you will use UIMenuBuilder in the app delegate to build the entire possible menu structure.

Then there will be validation code in the app delegate and each of your possible root view controllers. Each root view controller will only validate the items it can handle. For those it can't it calls super. And the app delegate handles what it can and for those it can't it disables them.

With that general approach, if Scene A is in focus, then it validates its own specific menu item and punts to the app delegate for the rest. The app delegate will handle a few and disable the rest. This means that while Scene A is in focus, any menu items specific to Scene B will be disabled (in the app delegate by default).

Here's some example skeleton code:

In AppDelegate.m:

- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder {
    [super buildMenuWithBuilder:builder];

    if (builder.system != UIMenuSystem.mainSystem) {
        return;
    }

    // Remove unwanted standard File menu:
    [builder removeMenuForIdentifier:UIMenuOpenRecent];
    [builder removeMenuForIdentifier:UIMenuOpen];
    [builder removeMenuForIdentifier:UIMenuDocument];

    // Add some new File menu items
    UICommand *item1 = [UIKeyCommand commandWithTitle:@"Item 1" image:[UIImage systemImageNamed:@"some.symbol"] action:@selector(item1MenuAction:) input:@"I" modifierFlags:UIKeyModifierShift | UIKeyModifierCommand propertyList:nil];
    UICommand *item2 = [UIKeyCommand commandWithTitle:@"Item 2" image:[UIImage systemImageNamed:@"other.symbol"] action:@selector(item2MenuAction:) input:@"J" modifierFlags:UIKeyModifierShift | UIKeyModifierCommand propertyList:nil];

    UIMenu *extraItems = [UIMenu menuWithTitle:@""  image: nil identifier:nil options:UIMenuOptionsDisplayInline children:@[ item1, item2 ]]; // NO_I18N

[builder insertSiblingMenu:extraItems afterMenuForIdentifier:UIMenuClose];

    // Build everything else as needed
}

- (void)validateCommand:(UICommand *)command {
    // Handle App level items here
    if (
        command.action == @selector(someMenuAction:) ||
        command.action == @selector(otherMenuAction:) ||
        NO) {
        command.attributes &= ~UIMenuElementAttributesDisabled; // Always on
    } else if (
        command.action == @selector(anotherMenuAction:) ||
        NO) {
        if (someCondition) {
            command.attributes &= ~UIMenuElementAttributesDisabled;
        } else {
            command.attributes |= UIMenuElementAttributesDisabled;
        }
    } else {
        [super validateCommand:command];
    }
}

Now add a validateCommand: to each of your root view controller classes. It should validate each of its own commands as needed. Call super for all other commands.

Tedious but straight forward.

Amazing. Really thanks. So I make the superset in the app delegate, and if nothing in the active window handles it, the item is disabled.

Very very much obliged.

Generally the menu items in the menu bar should contain the complete set of menu items that can be used in your app. They should remain stable even as users navigate across other scenes of your app.

As mentioned by others, you can implement the action method for each command within an individual view controller. Because UIKit traverses the responder chain to find a target for the command, and the active scene drives whatever the app's current chain is, then the command would target your view controller when its scene is active.

Generally commands are considered performable if a responder in the chain implements that action (which causes -canPerformAction:withSender: to return YES for that action). If no responders in the chain implement that action, then no responder will return YES for canPerformAction, and therefore the command won't be performable (and it will appear greyed out in the menu bar).

If you want custom logic for when the command is performable (such as having "Close Tab" greyed out in a web browser if there are no open tabs), you can override canPerformAction and return NO. Make sure to call super as well for actions that your responder doesn't care about.

-validateCommand: should be used for customizing the appearance of the command (e.g. title, image) based on the current context. For example, you may use it to have a toggleFavorite: command have the title "Add to Favorites" or "Remove from Favorites" depending on whether the focused item is in the user's favorites. It's not recommended to use validateCommand to add/remove the disabled attribute, as that will only cause the appearance of the command to be affected, but it may still be performed on the keyboard. The only way to guarantee that it won't be performed is to override canPerformAction.

That last paragraph was very helpful. I hadn’t realized I should be using canPerformSelector for most of the logic I currently have in validateCommand. It makes sense now that you point it out.

I looked into migrating some of my existing validateCommand: code into canPerformAction:sender: and I hit a roadblock. In one of my cases I use the same selector for a whole set of menus. I use the command’s propertyList value to act accordingly. This is fine in validateCommand: since I have access to the command. But in canPerformAction:sender I only get the selector. The sender is a private type called _UIMenuBarItem. I can see in the debugger that there is a reference to the command and the propertyList but there’s no public API to get to either.

BTW - that is the result while testing the Mac Catalyst version of the app.

Is there a tech note for menuBuilder?
 
 
Q