如何使用@EnvironmentObject在SwiftUI中在AppDelegate、SceneDelegate和Views之间共享数据

10
在SwiftUI 5.1中,我想使用AppDelegate创建一个userData对象。 userData还将包含BLE广告数据,这些数据也将从AppDelegate中更新。这些数据应该对UI可用,以便显示这些值。
在AppDelegate中,我使用:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    private var centralManager : CBCentralManager!
    var userData: UserData!

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        userData = UserData()

        return true
    }

在SceneDelegate中,我希望使用以下方法将传递到视图中:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    @EnvironmentObject var userData: UserData

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Use a UIHostingController as window root view controller
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView:
                BeaconList().environmentObject(userData)
            )

            self.window = window
            window.makeKeyAndVisible()
        }
    }

编译没有问题,但在运行代码时出现以下错误:

Thread 1: 致命错误: 在View.body之外读取EnvironmentObject

如果我删除

.environmentObject(userData)

我会得到

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

实际上,我试图从AppDelegate创建和更新userData对象,并从SceneDelegate和下面的视图中显示。

我应该如何实现这个?


为什么你不能只使用 SceneDelegate 呢?这是苹果要求处理此类问题的方式。(也许你发现了一种没有覆盖到的情况,但我怀疑这种情况存在。) - user7014451
@dfd 这样做会不会在每次创建新场景时都创建一个新的 userData 实例? - Peter Schorn
@PeterSchorn 你可能是对的。我的评论似乎是在 SwiftUI 1.0 beta 3 的时候,那已经是很久以前了。 :-) 我现在正在开发一个仅适用于 iPad 的 UIKit 应用程序,在其中我正在处理支持多个实例(或场景)和最终的拖放。看着我上面的评论,我仍然坚持它的意图 - 在大多数情况下,如果不是全部,苹果希望应用程序的每个场景或实例都有自己的沙盒,而不是所有实例都有。 - user7014451
2个回答

7
也许您可以使用 UIApplication.shared.delegate 来获取 AppDelegate 并访问用户数据:
class AppDelegate: UIResponder, UIApplicationDelegate {

    var userData: UserData!

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        userData = UserData()
        return true
    }
}

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        let userData = (UIApplication.shared.delegate as! AppDelegate).userData

        // Use a UIHostingController as window root view controller
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(userData))
            self.window = window
            window.makeKeyAndVisible()
        }
    }

谢谢,我按照AppDelegate的方法做了,现在正常运行。 - AnErd
这是一种违反SOLID原则并将AppDelegate作为全局单例的可怕方式。不幸的是,我没有更好的建议 - 苹果强制我们采用糟糕的设计,因为它不会给我们任何能够正确注入依赖项到场景代理中的能力。 - Alex Bush

1

您可以使用UISceneSession上的userInfo进行注入。

但是,这不会在每个应用程序会话中调用。如果iOS能够缓存您的会话,则在下一次应用程序启动时,它将不会通过应用程序委托传递。因此,您需要适当地缓存从应用程序委托传递到场景委托的任何信息。

// AppDelegate.swift
var client: Client

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        client = Client()
        return true
    }

func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        connectingSceneSession.userInfo = ["client": client]
        let config = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
        config.delegateClass = SceneDelegate.self
        config.sceneClass = UIWindowScene.self
        return config
    }

// SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
  if let client = session.userInfo?["client"] as? Client {
    ...
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接