You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 04:07:42