Implement preferences menu entry in MacCatalyst

I have an app which uses SwiftUI and Mac Catalyst. When running on a Mac I want to provide a preferences menu entry with the usual keyboard shortcut Command + ,. An implementation via the Settings bundle is out of question since my preferences are too complex for this.

Here is a reduced example of my implementation:

import SwiftUI

@main
struct PreferencesMenuTestApp: App {
  @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
  
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}


class AppDelegate: UIResponder, UIApplicationDelegate {
  
  override func buildMenu(with builder: UIMenuBuilder) {
    let preferencesCommand = UIKeyCommand(title: "Preferences…",
                                          action: #selector(showPreferences),
                                          input: ",",
                                          modifierFlags: .command)
    
//    let preferencesCommand = UIAction(title: "Preferences…") { action in
//      debugPrint("show preferences")
//    }
    
    let menu = UIMenu(title: "Preferences…",
                      options: .displayInline,
                      children: [preferencesCommand])
    
    builder.insertSibling(menu, afterMenu: .about)
  }
  
  
  @objc
  func showPreferences() {
    debugPrint("show preferences")
  }
}

The problem is that the menu entry is disabled. Obviously the provided selector is not recognised. When I mark the AppDelegate with @main, then the menu entry is enabled. Of course then the app's window is empty. When I switch to the UIAction implementation (the out commented code) it works fine. But since one cannot provide a keyboard shortcut for UIActions this is not a good solution.

What am I missing? How would one implement a preferences menu entry that actually works?

The preferences (aka "settings") behavior is automatically enabled for your app when you use a Settings scene in your app:

https://developer.apple.com/documentation/swiftui/settings/

This will give you, on macOS, the standard "Settings…" menu item and the default Command-comma shortcut.

The Settings scene is generally implemented as a TabView, as demonstrated on the above-linked documentation page.

As you discovered UIAction just takes a block with no keyboard shortcut support and UIKeyCommand takes a selector and has no ability to set a target (UIKeyCommand just goes through the responder chain).

For a top level action like "Show Settings" invoked from the menu bar it seems silly to me that the entire responder chain needs to be enumerated but that's how they decided to implement it in Mac Catalyst. A NSMenuItem in AppKit world can specify a target but for whatever reason they made the context menu/menu bar APIs a little weird in UIKit/Mac Catalyst.

I believe the AppDelegate is supposed to participate in the responder chain but don't think it does on Mac Catalyst which is why the menu bar item isn't validating. Add your showPreferences method on UIApplication in a Swift extension or Objective-C category instead (UIApplication is in the responder chain).

So long as nothing else in your responder chain implements a selector that collides with that name the UIApplication should always get it.

Did you find a good solution? Having the same issues.

Implement preferences menu entry in MacCatalyst
 
 
Q