0

I'm creating an NSViewRepresentable view to use an NSView in a SwiftUI code. And I need the NSViewRepresentable code to customize app's main menu via NSApp.mainMenu property.

But turns out the the eligible AppKit code for the task is not working in SwiftUI environment. By debugging the main menu I see that the items I add to the menu appears in the NSMenu instance but those items aren't showing in SwiftUI app menu, yet they show up in AppKit app menu.

Debugging shows that in SwiftUI environment the app's main menu uses SwiftUI.AppKitMainMenuItem object instead of NSMenuItem. But it is an Apple's private class I can not use.

How to achieve that in SwiftUI environment? I really need this to be done using Cocoa code base in my NSViewRepresentable class because I'm developing a universal extension for AppKit, UIKit and SwiftUI.

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Vitalii Vashchenko
  • 1,777
  • 1
  • 13
  • 23

2 Answers2

1

SwiftUI macOS Main Menu

Xcode 14.0+, macOS Monterey 12.5.1

To remove all five default menu items from main menu use the following approach:

import SwiftUI

class AppDelegate: NSObject, NSApplicationDelegate {
            
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        
        let menus = ["File","Edit","View","Window","Help"]
        
        menus.forEach { menu in
            NSApp.mainMenu?.item(withTitle: menu).map {
                NSApp.mainMenu?.removeItem($0)
            }
        }
    }
}

Then put @NSApplicationDelegateAdaptor property wrapper inside SwiftUI @main App's struct. To generate a custom menu item, use .commands {...} modifier.

@main struct YourApp: App {
    
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {

        WindowGroup {
            ContentView()
        }
        .commands {
            CommandMenu("Custom") {
                Button("Add Item", action: { } )
                    .keyboardShortcut("N")
                Divider()
                Button("Edit Item", action: { } )
                    .keyboardShortcut("E")
            }
        }
    }
}

Or, you can put @NSApplicationDelegateAdaptor property wrapper inside ContentView struct, instead of YourApp @main struct.

@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
  • As I have said, I really need this to be done using AppKit objects only because I am developing a universal Swift package. It has no @main App SwiftUI structure in it. – Vitalii Vashchenko Aug 28 '22 at 11:13
  • Post your code, please. – Andy Jazz Aug 28 '22 at 13:29
  • 1
    I don't know what code I should post. I have a method that creates a bunch of submenus in the app main menu. When my view is instantiated, I call that method. Everything works fine in AppKit environment and those submenus are added in app main menu. But it won't work when I wrap the view in NSViewRepresentable in SwiftUI environment - those submenus are added internally (main menu items array has them) but the menu won't show them. – Vitalii Vashchenko Aug 28 '22 at 13:57
  • Ok, Have you tried NSApplicationDelegateAdaptor? – Andy Jazz Aug 28 '22 at 14:13
  • 1
    I have no App structure in my Swift package. The package just providing the NSViewRepresentable view that can be used out of the package by an app that adds the package. So everything must be done on the NSViewRepresentable level. I have a guess that it's a bug of SwiftUI. – Vitalii Vashchenko Aug 28 '22 at 14:17
  • Vitalii, you're right. I tried to implement it outside of App struct – there's no effect. – Andy Jazz Aug 28 '22 at 16:27
  • 1
    I have filed a bug report to Apple. I believe it's a SwiftUI bug. – Vitalii Vashchenko Aug 28 '22 at 21:27
1

I have looked into this as well - It is probably a bug on swiftUI that keeps clobbering the menu.

Anyhow - have a look at this thread: SwiftUI Update the mainMenu [SOLVED] kludgey

Put it in a DispatchQueue.main.async closure in the AppDelegate.applicationWillUpdate function

import Foundation
import AppKit

public class AppDelegate: NSObject, NSApplicationDelegate {
    public func applicationWillUpdate(_ notification: Notification) {
        DispatchQueue.main.async {
            let currentMainMenu = NSApplication.shared.mainMenu

            //
            // Do your menu stuff here
            //
            // The below removes the Edit menu
            //

            let editMenu: NSMenuItem? = currentMainMenu?.item(withTitle: "Edit")
            if nil != editMenu {
                NSApp.mainMenu?.removeItem(editMenu!)
            }
        }
    }
}