Reputation: 3452
My MacOS app doesn't have any text editing possibilities. How can I hide the Edit
menu which is added to my app automatically? I'd prefer to do this in SwiftUI.
I would expect the code below should work, but it doesn't.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
CommandGroup(replacing: .textEditing) {}
}
}
}
Upvotes: 4
Views: 3555
Reputation: 3831
Neither of the answers suggesting KVO nor answers suggesting responding to application lifecycle methods (other than applicationDidFinishLaunching
) worked for me (the result was either glitchy, delayed or sometimes just plain wouldn't work.)
What did end up working for me (targeting macOS 14.0) is just HIDING the offending main menu items:
@objc
public class AppDelegate: NSObject, NSApplicationDelegate {
public func applicationDidFinishLaunching(_ notification: Notification) {
hideUnusedMenuItems()
}
}
private extension AppDelegate {
func hideUnusedMenuItems() {
["File", "Edit", "Window", "View"]
.compactMap { NSApp.mainMenu?.item(withTitle: $0) }
.forEach { $0.isHidden = true }
}
}
and then in your SwiftUI
App
struct:
@main
struct MyMenuItemHidingApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// ...
}
Fun fact: The same works with any of the main-menu's submenu-items.
Still obviously not great that menu items have to be found by name, which will obviously break this implementation when the user has a none-english version of macOS...
I'll die on that hill another day.
Upvotes: 0
Reputation: 189
You should be able to hide whole menus by modifying @waggles's answer a bit.
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
// SwiftUI updates menu on occlusion state change
func applicationDidChangeOcclusionState(_ notification: Notification) {
["Edit", "View"].forEach { name in
NSApp.mainMenu?.item(withTitle: name).map { NSApp.mainMenu?.removeItem($0) }
}
}
func applicationDidFinishLaunching(_ notification: Notification) {
if let app = notification.object as? NSApplication, app.windows.count > 0 {
// For some reason if window delegate is not set here,
// occulsionState notification fires with a delay on
// window unhide messing up the menu-update timing.
app.windows.first?.delegate = self
}
}
}
Upvotes: 0
Reputation: 124
For those of you that are looking for any updates on this - have a look at this question that I asked (and answered myself):
SwiftUI Update the mainMenu [SOLVED] kludgey
The way I got around it was to 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
let editMenu: NSMenuItem? = currentMainMenu?.item(withTitle: "Edit")
if nil != editMenu {
NSApp.mainMenu?.removeItem(editMenu!)
}
}
}
}
It took me a good 4 days of searching and attempting things :) - typical that it came down to a 2 line code change
Upvotes: 2
Reputation: 2854
The current suggestions failed for me when SwiftUI updated the body of a window.
Solution:
Use KVO and watch the NSApp
for changes on \.mainMenu
. You can remove whatever you want after SwiftUI has its turn.
@objc
class AppDelegate: NSObject, NSApplicationDelegate {
var token: NSKeyValueObservation?
func applicationDidFinishLaunching(_ notification: Notification) {
// Remove a single menu
if let m = NSApp.mainMenu?.item(withTitle: "Edit") {
NSApp.mainMenu?.removeItem(m)
}
// Remove Multiple Menus
["Edit", "View", "Help", "Window"].forEach { name in
NSApp.mainMenu?.item(withTitle: name).map { NSApp.mainMenu?.removeItem($0) }
}
// Must remove after every time SwiftUI re adds
token = NSApp.observe(\.mainMenu, options: .new) { (app, change) in
["Edit", "View", "Help", "Window"].forEach { name in
NSApp.mainMenu?.item(withTitle: name).map { NSApp.mainMenu?.removeItem($0) }
}
// Remove a single menu
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 {
//...
}
}
Thoughts:
SwiftUI either has a bug or they really don't want you to remove the top level menus in NSApp.mainMenu
. SwiftUI seems to reset the whole menu with no way to override or customize most details currently (Xcode 13.4.1). The CommandGroup(replacing: .textEditing) { }
-esque commands don't let you remove or clear a whole menu. Assigning a new NSApp.mainMenu
just gets clobbered when SwiftUI wants even if you specify no commands.
This seems like a very fragile solution. There should be a way to tell SwiftUI to not touch the NSApp.mainMenu
or enable more customization. Or it seems SwiftUI should check that it owned the previous menu (the menu items are SwiftUI.AppKitMainMenuItem
). Or I'm missing some tool they've provided. Hopefully this is fixed in the WWDC beta?
(In Xcode 13.4.1 with Swift 5 targeting macOS 12.3 without Catalyst.)
Upvotes: 6
Reputation: 121
It is possible to remove application menus using an NSApplicationDelegate
. This approach may break in future macOS versions (e.g. if the position of the Edit menu were to change), but does currently work:
class MyAppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
let indexOfEditMenu = 2
func applicationDidFinishLaunching(_ : Notification) {
NSApplication.shared.mainMenu?.removeItem(at: indexOfEditMenu)
}
}
@main
struct MyApp: App {
@NSApplicationDelegateAdaptor private var appDelegate: MyAppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
// ...
}
}
}
For Catalyst-based macOS apps, the approach is similar to that above, except that a UIApplicationDelegate
deriving from UIResponder
is used:
class MyAppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
override func buildMenu(with builder: UIMenuBuilder) {
/// Only operate on the main menu bar.
if builder.system == .main {
builder.remove(menu: .edit)
}
}
}
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor private var appDelegate: MyAppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
// ...
}
}
}
Upvotes: 1
Reputation: 12165
To my knowledge you cannot hide the whole menu, you can just hide element groups inside of it:
.commands {
CommandGroup(replacing: .pasteboard) { }
CommandGroup(replacing: .undoRedo) { }
}
Upvotes: 7