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

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

我有一个使用 NSApplicationDelegateAdaptor 的工作解决方案,其中 statusBar 在 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")
    }
}

这项工作非常好。但是,它仍然使用旧的应用程序委托。我试图在 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()
        }
    }
}

同样的序列(在应用程序委托中工作)在应用程序结构的初始化中失败,在包含 statusBar = NSStatusBar.init() 的行上的 StatusBarController 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)),line 133.
(lldb)

但是,在设置弹出窗口的尺寸之后放置 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()
        }
    }
}

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

其他相关代码如下。这是 StatusBarController:

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

内容视图:

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

空视图:

struct EmptyView: View {
    var body: some View {
        Text(/*@START_MENU_TOKEN@*/"Hello,World!"/*@END_MENU_TOKEN@*/)
    }
}
iCMS 回答:如何在 SwiftUI 应用程序生命周期中将 statusBar 传递给 popover 中的 contentView?

暂时没有好的解决方案,如果你有好的解决方案,请发邮件至:iooj@foxmail.com
本文链接:https://www.f2er.com/116546.html

大家都在问