SwiftUI macOS菜单栏应用:如何将ContentView中的变量传递至StatusBarController类
解决macOS菜单栏应用中StatusBarController访问ContentView变量的问题
我明白你现在遇到的问题——想让StatusBarController根据ContentView里Toggle的状态来控制popover是否在点击外部时关闭,但用EnvironmentObject的时候崩溃了。咱们先理清楚问题出在哪,再一步步修复。
首先,之前的错误根源有两个:
- 你没有把StatusBarController的实例正确注入到ContentView的环境中,导致ContentView找不到对应的ObservableObject
- StatusBarController里的代码结构有问题,
if lock { ... }直接写在类作用域里,这是不合法的Swift语法,应该放到方法或者属性观察者里
接下来是具体的修复步骤:
步骤1:重构StatusBarController,实现状态监听
把StatusBarController改成正确的ObservableObject,并且通过属性观察者监听lock的变化,来开启/关闭事件监视器:
import AppKit import SwiftUI class StatusBarController: ObservableObject { private var eventMonitor: EventMonitor? private let popover: NSPopover // 用@Published修饰,让ContentView能监听变化 @Published var lock: Bool = false { // 属性观察者,当lock变化时自动执行逻辑 didSet { if lock { // 锁定状态:移除事件监视器,点击外部不关闭popover eventMonitor?.stop() eventMonitor = nil } else { // 非锁定状态:添加事件监视器,点击外部关闭popover unlockpopover() } } } init(_ popover: NSPopover) { self.popover = popover // 初始化时默认开启监视器 unlockpopover() } func unlockpopover() { eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in guard let self = self else { return } // 点击外部时关闭popover if let eventWindow = event.window, eventWindow != self.popover.contentViewController?.view.window { self.popover.performClose(nil) } } eventMonitor?.start() } } // 补充EventMonitor类(如果你的项目里还没有的话) class EventMonitor { private var monitor: Any? private let mask: NSEvent.EventTypeMask private let handler: (NSEvent?) -> Void init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) { self.mask = mask self.handler = handler } func start() { monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) } func stop() { if let monitor = monitor { NSEvent.removeMonitor(monitor) self.monitor = nil } } }
步骤2:在AppDelegate中正确注入EnvironmentObject
在创建ContentView的时候,把已经初始化好的StatusBarController实例注入到ContentView的环境中:
import Cocoa import SwiftUI @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { var popover = NSPopover() var statusBar: StatusBarController? func applicationDidFinishLaunching(_ aNotification: Notification) { // 先初始化StatusBarController,传入popover statusBar = StatusBarController(popover) // 创建ContentView,并注入StatusBarController实例作为EnvironmentObject let contentView = ContentView() .environmentObject(statusBar!) // 设置popover的内容视图 popover.contentSize = NSSize(width: 360, height: 160) popover.contentViewController = NSHostingController(rootView: contentView) // 创建状态栏图标(根据你的需求调整) let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = statusItem.button { button.image = NSImage(systemSymbolName: "house", accessibilityDescription: "Menu App") button.action = #selector(togglePopover(_:)) } } @objc func togglePopover(_ sender: Any?) { if popover.isShown { popover.performClose(sender) } else { if let button = statusBar?.statusItem.button { popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) } } } func applicationWillTerminate(_ aNotification: Notification) { // 清理资源 statusBar?.eventMonitor?.stop() } }
步骤3:修改ContentView,正确使用EnvironmentObject
移除ContentView里错误的.environmentObject(self.test),因为我们已经在AppDelegate里注入了:
import SwiftUI struct ContentView: View { // 用@EnvironmentObject获取StatusBarController实例 @EnvironmentObject var statusBar: StatusBarController var body: some View { VStack{ Text("Hello World") Toggle(isOn: $statusBar.lock, label: { Text("锁定Popover") // 给Toggle添加标签,提升用户体验 }) .toggleStyle(SwitchToggleStyle()) .padding() } .frame(width: 360.0, height: 160.0, alignment: .top) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { // 预览时需要提供一个StatusBarController实例 ContentView() .environmentObject(StatusBarController(NSPopover())) } }
关键修复点说明
- EnvironmentObject注入时机:必须在创建ContentView的时候就注入,而不是在ContentView内部,否则视图找不到对应的对象。
- 属性观察者
didSet:用来监听lock的变化,自动处理事件监视器的开启和关闭,不需要手动在类作用域里写if语句。 - 弱引用
[weak self]:在EventMonitor的handler里使用弱引用,避免循环引用导致内存泄漏。
这样修改后,你就能通过ContentView里的Toggle控制StatusBarController的lock变量,进而控制popover是否在点击外部时关闭了。
内容的提问来源于stack exchange,提问作者Eric
相关产品推荐
相关产品推荐

