如何在 SwiftUI 应用程序生命周期中将 statusBar 传递给 popover 中的 contentView?

sil*_*fer 7 macos nsstatusbar swift swiftui

我正在尝试使用 SwiftUI 2.0(应用程序结构)中引入的 SwiftUI 应用程序生命周期创建一个菜单栏应用程序,其中 SwiftUI 代码中的按钮和操作将更新状态栏项目的文本。我正在创建一个状态栏项目,它将触发一个包含 SwiftUI 视图的弹出窗口。根据我的理解,最好的方法是将 statusBar 传递给 ContentView 的子级,后者将能够使用 statusBar 作为环境对象。但是,我遇到一个我不明白的问题,即在应用程序委托中工作的代码在 SwiftUI 2.0 init 函数中失败(不清楚原因)。我希望我已将下面所有相关的代码包含在一个单独的示例中,以触及问题的核心。

我有一个工作解决方案,使用NSApplicationDelegateAdaptorstatusBar 在 ContentView 之前初始化,并且 statusBar 只是作为环境对象传递到 ContentView:

import SwiftUI
import AppKit

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

    var body: some Scene {
        Settings{
            EmptyView()
        }
    }
}

class AppDelegate: NSObject, NSApplicationDelegate {
    var popover = NSPopover.init()
    var statusBar: StatusBarController?

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the Status Bar Item with the Popover
        statusBar = StatusBarController.init(popover)

        // Pass the Status Bar Item as an environment object to children of ContentView
        let contentView = ContentView()
            .environmentObject(statusBar!)

        popover.contentViewController = NSHostingController(rootView: contentView)
        popover.contentSize = NSSize(width: 360, height: 360)

        statusBar?.updateStatusBarText(text: "app delegate")
    }
}
Run Code Online (Sandbox Code Playgroud)

这工作相当不错。但是,它仍然使用旧的应用程序委托。我试图在 SwiftUI 2.0 中使用应用程序结构的新 init() 函数,而不是将应用程序启动委托给应用程序(较旧的)委托。因此,我将上面的代码移至 init() 函数中:

import SwiftUI
import AppKit

@main
struct test_status_bar_appApp: App {
    var popover = NSPopover.init()
    var statusBar: StatusBarController?

    init () {

        // ISSUE: putting line `statusBar = StatusBarController.init(popover)`
        // (so that it can be passed in as an environment variable)
        // causes error
        statusBar = StatusBarController.init(popover)

        let contentView = ContentView()
            .environmentObject(statusBar!)

        popover.contentViewController = NSHostingController(rootView: contentView)
        popover.contentSize = NSSize(width: 360, height: 360)

        statusBar?.updateStatusBarText(text: "init before")
    }

    var body: some Scene {
        Settings{
            EmptyView()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这个相同的序列(在应用程序委托中工作)在应用程序结构的 init 中失败,在包含以下行的 StatusBarController init 中失败statusBar = NSStatusBar.init()

错误本身为Thread 1: hit program assert,以及其他错误详细信息:

Assertion failed: (CGAtomicGet(&is_initialized)), function CGSConnectionByID, file /System/Volumes/Data/SWE/macOS/BuildRoots/e90674e518/Library/Caches/com.apple.xbs/Sources/SkyLight/SkyLight-588.1/SkyLight/Services/Connection/CGSConnection.mm, line 133.
Assertion failed: (CGAtomicGet(&is_initialized)), function CGSConnectionByID, file /System/Volumes/Data/SWE/macOS/BuildRoots/e90674e518/Library/Caches/com.apple.xbs/Sources/SkyLight/SkyLight-588.1/SkyLight/Services/Connection/CGSConnection.mm, line 133.
(lldb)
Run Code Online (Sandbox Code Playgroud)

但是,在设置弹出窗口的尺寸之后放置 StatusBarController 的初始化确实可以在菜单栏中创建状态栏项目...但不幸的是,这种方式无法作为环境对象传递给 ContentView。其代码如下:

import SwiftUI
import AppKit

@main
struct test_status_bar_appApp: App {
    var popover = NSPopover.init()
    var statusBar: StatusBarController?

    init () {
        let contentView = ContentView()
//            .environmentObject(statusBar!)

        popover.contentViewController = NSHostingController(rootView: contentView)
        popover.contentSize = NSSize(width: 360, height: 360)

        // BUT... putting the status bar HERE works,
        // but is sadly not able to be passed
        // as an environmentObject into the ContentView above
        statusBar = StatusBarController.init(popover)
        statusBar?.updateStatusBarText(text: "init after")
    }

    var body: some Scene {
        Settings{
            EmptyView()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

谁能解释发生了什么事,为什么会发生这种情况,以及如何解决它?目前,我确实有一个可行的解决方案,我可以通过旧的应用程序委托将 statusBar 作为环境对象传递到 ContentView 中。但理想情况下,我会使用 SwiftUI 生命周期中 app 结构的较新 init() 函数,因为这似乎是在新结构中执行此操作的方法,同时保留将 statusBar 作为环境对象传递给 ContentView 的能力。

其他相关代码如下。这是状态栏控制器:

class StatusBarController: ObservableObject {
    private var statusBar: NSStatusBar
    private var statusItem: NSStatusItem
    private var popover: NSPopover
    
    init(_ popover: NSPopover) {
        self.popover = popover

        statusBar = NSStatusBar.init()
        statusItem = statusBar.statusItem(withLength: 80)
        
        if let statusBarButton = statusItem.button {
            statusBarButton.wantsLayer = true
            statusBarButton.layer?.masksToBounds = true
            statusBarButton.layer?.cornerRadius = 5
        }
    }
    
    func updateStatusBarText(text: String) {
        statusItem.button?.title = text
    }
}
Run Code Online (Sandbox Code Playgroud)

内容查看:

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var statusBar: StatusBarController
    
    var body: some View {
        VStack{
            Text("Hello, world!").padding()
            Button("Ok", action: {
                print("clicked button in swiftui")
                // NOTE: example of trying to use statusBar environment object
                // statusBar.updateStatusBarText(text: "IT's WORKING!!!")
            }).padding()
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Run Code Online (Sandbox Code Playgroud)

空视图:

struct EmptyView: View {
    var body: some View {
        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
    }
}
Run Code Online (Sandbox Code Playgroud)

rik*_*chi 0

尝试将代码放入 DispatchQueue.main.async { code }

例如:

DispatchQueue.main.async {
   let statusBar = NSStatusBar.system
   var statusBarItem : NSStatusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
        
   statusBarItem.button?.image = NSImage(systemSymbolName: "hare", accessibilityDescription: nil)
   statusBarItem.button?.imageScaling = .scaleProportionallyDown
        
   statusBarItem.button?.action = #selector(self.openMenuBar(sender:))
   statusBarItem.button?.target = self
   statusBarItem.button?.sendAction(on: [.leftMouseDown])
}
Run Code Online (Sandbox Code Playgroud)

为了完整起见,在此示例中,单击处理程序应按以下方式编写:

@objc func openMenuBar(sender: Any) {
    print("Open MenuBar")
}
Run Code Online (Sandbox Code Playgroud)