SwiftUI's handling of the Mac menu bar is still pretty bad

SwiftUI's handling of the Mac menu bar is still pretty bad

While trying out a few things around the SwiftUI document handling and lifecycle in a macOS application, I came across a pretty bad issue. Not only did it not do as the API promised, it actually messed with the menu in ways that would be unrecoverable to a SwiftUI lifecycle application.

  • Menu items disappear from the menu bar;
  • Completely empty menu items with the text "NSMenuItem" are inserted in unexpected places, with no way to remove them.

Background

As part of the opt-in SwiftUI application lifecycle, SwiftUI offers scene management. And, as part of that, it offers Commands API. Named after the analogous UIKit concept, on macOS Commands roughly translates to key equivalents. In other words, when you add Commands to a SwiftUI scene, When you add Commands to a SwiftUI scene, your menu bar gains new menu items that trigger the callback of your choice when selected.

This API lets you do most of the things. But, in the best of the framework's traditions, while easy things become easier, the trickier things become impossible. And, as it turns out, some things will simply break along the way.

Symptoms

The issues I found as part of this exploration are 100% reproducible on macOS Sequoia 15.2.

To reproduce the issue, I created a sample document-based SwiftUI lifecycle application via the Xcode template. This is what the standard File menu that came with the template looked like before any modifications. So far, so good.

The default look of the File menu in a document-based SwiftUI app

I then created multiple sets of Commands and added them to the default scene, one at a time. For testing purposes, I imagined my document-based app would allow you to open folders as well as documents – a pretty common task for a Mac app. I wanted to ideally insert it alongside the built-in Open menu item in the File menu in the menu bar.

It turns out, at least two of them turned out to be problematic. Replacing either CommandGroupPlacement.newItem or CommandGroupPlacement.saveItem . Both caused issues with the File menu.

Replacing the New item

First, I declared the Commands.

struct FolderCommandsReplacingNewItem: Commands {
    var body: some Commands {
        CommandGroup(replacing: .newItem) {
            Button("Open Folder…") {
                print("Open Folder…")
            }
            .keyboardShortcut("O", modifiers: [.command, .option])
        }
    }
}

I then added the new commands to the default scene of my application:

@main
struct SwiftUICommandBugShowcaseApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: SwiftUICommandBugShowcaseDocument()) { file in
            ContentView(document: file.$document)
        }
        .commands {
            FolderCommandsReplacingNewItem()
        }
    }
}

The expected result is quite obvious: you'd expect that instead of the New menu item, you'd end up with the Open Folder item, followed by Open, Open Recent, Close, Save, etc.

The outcome of replacing the New item

This command did indeed replace the New menu item, but it also completely get rid of the Open menu item – something I didn't expect from the API contract.

The trouble didn't end there, however. SwiftUI also kindly appended an empty disabled NSMenuItem to the end of the menu.

From my previous journey as a maintainer of the menu system infrastructure on macOS, the literal "NSMenuItem" title is typically indicative of initializing an NSMenuItem using the initializer with no arguments and not providing a valid title. It'd get replaced with the title or the attributed title you provided during configuration. It appears this menu item is abandoned and not updated from the default values at all.

Replacing the Save item

I then decided to test a few other item placements. Here's saveItem, for example:

struct FolderCommandsReplacingSaveItem: Commands {
    var body: some Commands {
        CommandGroup(replacing: .saveItem) {
            Button("Open Folder…") {
                print("Open Folder…")
            }
            .keyboardShortcut("O", modifiers: [.command, .option])
        }
    }
}
The outcome of replacing the Save item

Unlike what the API advertises, this will not actually replace the Save menu item. Instead, it will get rid of the Save menu item, replace it with an empty disabled NSMenuItem, and append the Open Folder (the sample) item to the end of the menu. Complete and utter chaos!

Resolution?

Needless to say, these issues make it very challenging to develop a SwiftUI-first Mac application, as the menu experience on macOS is one that distinguishes a quality app from… well… Electron and web. It's a shame that we still can't build delightful menu experiences using SwiftUI.

I've posted the sample project on GitHub here: https://github.com/philptr/SwiftUIMacCommandBug.

I've also reported this to the SwiftUI team (FB16145855). We'll see where this goes.