隐藏 SwiftUI / MacOS 应用程序的编辑菜单

发布于 2025-01-11 07:34:57 字数 350 浏览 0 评论 0原文

我的 MacOS 应用程序没有任何文本编辑功能。如何隐藏自动添加到我的应用程序中的编辑菜单?我更喜欢在 SwiftUI 中执行此操作。

我希望下面的代码应该可以工作,但事实并非如此。

@main
struct MyApp: App {

var body: some Scene {
    WindowGroup {
        ContentView()
    }.commands {
        CommandGroup(replacing: .textEditing) {}
    }
}

}

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) {}
    }
}

}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

悟红尘 2025-01-18 07:34:57

据我所知,您无法隐藏整个菜单,您可以隐藏其中的元素组:

    .commands {
        CommandGroup(replacing: .pasteboard) { }
        CommandGroup(replacing: .undoRedo) { }
    }

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) { }
    }
递刀给你 2025-01-18 07:34:57

当 SwiftUI 更新窗口主体时,当前的建议对我来说失败了。

解决方案:

使用 KVO 并观察 NSApp\.mainMenu 的更改。轮到 SwiftUI 后,您可以删除任何您想要的内容。

@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 { 
        //... 
    }
}

想法:

SwiftUI 要么有错误,要么他们真的不希望您删除 NSApp.mainMenu 中的顶级菜单。 SwiftUI 似乎重置了整个菜单,目前无法覆盖或自定义大多数细节(Xcode 13.4.1)。 CommandGroup(replacing: .textEditing) { } 式命令不允许您删除或清除整个菜单。即使您没有指定任何命令,当 SwiftUI 需要时,分配新的 NSApp.mainMenu 也会被破坏。

这似乎是一个非常脆弱的解决方案。应该有一种方法告诉 SwiftUI 不要触摸 NSApp.mainMenu 或启用更多自定义。或者 SwiftUI 似乎应该检查它是否拥有以前的菜单(菜单项是 SwiftUI.AppKitMainMenuItem )。或者我缺少他们提供的一些工具。希望这个问题能在 WWDC 测试版中得到解决吗?

(在带有 Swift 5 的 Xcode 13.4.1 中,针对 macOS 12.3,没有 Catalyst。)

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.)

想念有你 2025-01-18 07:34:57

对于那些正在寻找任何更新的人 - 请查看我提出的这个问题(并自己回答):

SwiftUI 更新 mainMenu [已解决] kludgey

我解决这个问题的方法是把它位于 AppDelegate applicationWillUpdate 函数的 DispatchQueue.main.async 闭包中

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!)
            }
        }
    }
}

我花了 4 天的时间进行搜索和尝试:) - 通常只需更改 2 行代码即可

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

与酒说心事 2025-01-18 07:34:57

对于本机 (Cocoa) 应用程序

可以使用 NSApplicationDelegate 删除应用程序菜单。此方法可能会在未来的 macOS 版本中中断(例如,如果编辑菜单的位置发生更改),但目前确实有效:

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 {
      // ...
    }
  }
}

对于 Catalyst (UIKit) 应用程序

对于基于 Catalyst 的 macOS 应用程序,该方法是与上面类似,不同之处在于使用了从 UIResponder 派生的 UIApplicationDelegate

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 {
      // ...
    }
  }
}

For native (Cocoa) apps

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 (UIKit) apps

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 {
      // ...
    }
  }
}
烟柳画桥 2025-01-18 07:34:57

您应该能够通过稍微修改@waggles 的答案来隐藏整个菜单。

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
        }
    }
}

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
        }
    }
}
如此安好 2025-01-18 07:34:57

建议 KVO 的答案和建议响应应用程序生命周期方法(applicationDidFinishLaunching 除外)的答案都不适合我(结果要么是小故障、延迟,要么有时根本不起作用。

)为我工作(针对 macOS 14.0)只是隐藏有问题的主菜单项:

@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 }
    }
}

然后在您的 SwiftUI App 结构中:

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

    // ...
}

有趣的事实:同样适用于任何主菜单的子菜单项。

显然仍然不太好,必须通过名称找到菜单项,当用户使用非英语版本的 macOS 时,这显然会破坏此实现......

改天我会死在那个山上。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文