7

I'm (attempting) switching over my AppDelegate macOS app to the SwiftUI lifecycle - but can't seem to find out how to handle the CommandMenu. I just want to delete these default menu items (Fie, Edit, View, etc...). In the past, I would just delete them from the Storyboard - but I'm not using a storyboard here. Is there a way to delete these items in SwiftUI?

The items I want to delete:

The items I want to delete

I know how to add new items via:

.commands {
  MyAppMenus()
}  

But that just adds them inline with the existing menu items.

5 Answers 5

8

swiftUI -- override AppDelegate with your custom:

@main
struct PixieApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
///........
}

code of appDelegate:

final class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationWillUpdate(_ notification: Notification) {
        DispatchQueue.main.async {
            if let menu = NSApplication.shared.mainMenu {
                menu.items.removeFirst{ $0.title == "Edit" }
                menu.items.removeFirst{ $0.title == "File" }
                menu.items.removeFirst{ $0.title == "Window" }
                menu.items.removeFirst{ $0.title == "View" }
            }
        }
    }
}

result:

enter image description here

As truly this is bad solution.

  1. will not work on non-English localizations
  2. can be flickering on menu line

But i didn't found better one. =(

Sign up to request clarification or add additional context in comments.

6 Comments

This worked for me. I don't seem to have a problem with SwiftUI adding menu items back like the other commenter mentions, so I was able to stick this under applicationDidFinishLaunching. Note: applicationWillUpdate is called a lot, forever.
applicationDidFinishLaunching works until first refresh of menu ,thats why better applicationWillUpdate - to update once more when this menu will be refreshed.
for Swift 5, what will work for you is menu.items.removeAll { $0.title == "Edit" }
@outrowender it will work only for english language :) Just like in my answer.
After some messing around I found leaving the items and changing isHidden to be the most efficient approach. That way I can set the items once in ApplicationDidLoad and change my menu separately as needed.
|
4

Working off of the above, but using NotificationCenter vs KVO.

final class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        let unwantedMenus = ["File", "Edit"]
        
        let removeMenus = {
            unwantedMenus.forEach {
                guard let menu = NSApp.mainMenu?.item(withTitle: $0) else { return }
                NSApp.mainMenu?.removeItem(menu)
            }
        }

        NotificationCenter.default.addObserver(
            forName: NSMenu.didAddItemNotification,
            object: nil,
            queue: .main
        ) { _ in
            // Must refresh after every time SwiftUI re adds
            removeMenus()
        }

        removeMenus()
    }
}

1 Comment

Sadly this doesn't let me remove the File menu and add back my own, because this code removes my new File menu. I guess I just have make a different name for that menu in my App?
2

Until SwiftUI adds more support for adjusting menus, I think you have to worry about SwiftUI reseting the NSApp.mainMenu whenever it updates a window.body. I haven't tried every method for adjusting the mainMenu, but of the methods I tried, the flaw was that SwiftUI seems to have no check for whether it last set NSApp.mainMenu or if something else did.

So however you are managing the menu, update it after SwiftUI has.

Use KVO and watch the NSApp for changes on .mainMenu. Then make your changes with a xib, or reseting the whole thing, or editing SwiftUI's menus.

Example:

@objc
class AppDelegate: NSObject, NSApplicationDelegate {
    
    var token: NSKeyValueObservation?
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        
        // Adjust a menu initially
        if let m = NSApp.mainMenu?.item(withTitle: "Edit") {
            NSApp.mainMenu?.removeItem(m)
        }

        // Must refresh after every time SwiftUI re adds
        token = NSApp.observe(\.mainMenu, options: .new) { (app, change) in
            // Refresh your changes
            guard let menu = app.mainMenu?.item(withTitle: "Edit") else { return }
            app.mainMenu?.removeItem(menu)
        }
    }
}

struct MarblesApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some View { 
        //... 
    }
}

This seems to work in Xcode 13.4.1 with Swift 5 targeting macOS 12.3.

Hopefully Apple adds greater control soon. It seems Catalyst has other options. Or you can create a traditional AppKit app and insert the SwiftUI views into it.

2 Comments

This seems to work reliably. I’m having trouble adapting the solution to remove the File > Share menu item that comes with a SwiftUI document-based app. SwiftUI doesn’t provide a CommandGroupPlacement for the Share command, so I can’t remove it the official way. I can get the File menu with let fileMenu = items.first { $0.title == "File" } and the share menu command with fileMenu?.submenu?.items.first {$0.title == "Share"}. But I can't figure out how to use the KVO observer to detect when SwiftUI re-adds the Share command.
I couldn't come up with a simple solution. Maybe you can use NotificationCenter.default.publisher(for: NSMenu.didAddItemNotification, object: nil) instead of KVO and check each time if it is the item you want to remove (e.g. the "Share"). And then remove it when it is. I have found messing with the menus too cumbersome at this point.
0

You can remove command menu items through the AppDelegate file:

override func buildMenu(with builder: UIMenuBuilder) {
    super.buildMenu(with: builder)
    builder.remove(menu: .services)
    builder.remove(menu: .format)
    builder.remove(menu: .toolbar)
}

This thread on the Apple Developer forum might help as well: https://developer.apple.com/forums/thread/649096

2 Comments

That would work if I'm using Catalyst but I'm not - UIMenuBuilder isn't available on macOS.
@JoeBayLD Oh, sorry about that
-3
CommandGroup(replacing: CommandGroupPlacement.appVisibility, addition: {})

2 Comments

Please can you develop you answer?
Can you add some explanation why/how this solves the OP's question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.