SwiftUI 子视图未按预期使用 environmentObject 进行更新

SwiftUI Child Views not updating as expected with environmentObject

请查看我提供的示例,我尽可能准确地重新创建了我的模式,同时省略了与问题无关的细节。

我的视图模型中有 @Published 属性 个变量,在获取到 firebase 后这些变量是 updated/assigned。每次访问根视图或子视图之一时,获取逻辑就会运行(或从缓存中获取),然后将我的值映射到视图模型中的 @Published 字典。让我担心的是,我的 CardView 总是更新成功,而我的 AlternateCardView 仅在第一次加载时从我的字典中获取正确的值,但除非我终止应用程序,否则再也不会。

我是否遗漏了一个明显的最佳实践?有没有更好的方法来实现我的模式来避免这个错误?我希望我的 AlternateCardView 在检测到更改时进行更新,并且我已验证我的视图模型确实在更新值 - 它们只是没有转化为我的视图。

请注意:我还尝试过使用自定义结构的托管集合而不是示例中提供的文字字典来解决此解决方案。尽管如此,我描述的错误仍然存​​在 - 所以我确信这不是问题所在。我这样做是因为我认为它可以保证触发 objectWillChange,但我想知道我是否真的 运行 陷入了 SwiftUI 的怪诞讽刺之中。

我在 iOS15 iPhone 11 模拟器上使用 Xcode 版本 13.2.1、Swift5.1 和 运行。

内容视图:

struct ContentView: View {
    // ...
    var body: some View {
        VStack {
            RootView().environmentObject(ProgressEngine())
        }
    }
}

根视图:

struct RootView: View {
    @EnvironmentObject var userProgress: ProgressEngine

    var body: some View {
        VStack {
            NavigationLink(destination: ChildView().environmentObject(self.userProgress)) {
              CardView(progressValue: self.$userProgress.progressValues)
            }
        }
        .onAppear {
            self.userProgress.fetchAllProgress() // This is fetching data from firebase, assigns to my @Published properties
        }
    }
}

卡片视图:

// This view works and updates all the time, successfully - no matter how it is accessed
struct CardView: View {
    @EnvironmentObject var userProgress: ProgressEngine
    @Binding var progressVals: [String: CGFloat] // binding to a dict in my viewmodel
    var body: some View {
        VStack {
            // just unwrapping for example
            Text("\(self.userProgress.progressValues["FirstKey"]!)") 
        }
    }
}

子视图:

struct ChildView: View {
    @EnvironmentObject var userProgress: ProgressEngine
    @EnvironmentObject var anotherObject: AnotherEngine
    VStack {
        // I have tried this both with a ForEach and also by writing each view manually - neither works
        ForEach(self.anotherObject.items.indices, id: \.self) { index in 
            NavigationLink(destination: Text("another view").environmentObject(self.userProgress)) {
                // This view only shows the expected values on first load, or if I kill and re-load the app
                AlternateCardView(userWeekMap: self.$userProgress.weekMap)
            }
        }
    }
    .onAppear {
        self.userProgress.fetchAllProgress()
        self.userProgress.updateWeekMap()
}

备用卡片视图:

// For this example, this is basically the same as CardView, 
// but shown as a unique view to replicate my situation
struct AlternateCardView: View {
    @EnvironmentObject var userProgress: ProgressEngine
    @Binding var weekMap: [String: [String: CGFloat]] 
    var body: some View {
        VStack {
            // just unwrapping for example
            // defined it statically for the example - but dynamic in my codebase
            Text("\(self.userProgress.weekMap["FirstKey"]!["WeekKey1"]!)") 
        }
    }
}

查看模型:

class ProgressEngine: ObservableObject {

    // Accessing values here always works
    @Published var progressValues: [String: CGFloat] = [
        "FirstKey": 0,
        "SecondKey": 0,
        "ThirdKey": 0
    ]
    
    // I am only able to read values out of this the first time view loads
    // Any time my viewmodel updates this map, the changes are not reflected in my view
    // I have verified that these values update in the viewmodel in time,
    // To see the changes, I have to restart the app
    @Published var weekMap: [String: [String: CGFloat]] = [
        "FirstKey": [
            "WeekKey1": 0,
            "WeekKey2": 0,
            "WeekKey3": 0,
            .....,
            .....,
         ],
         "SecondKey": [
            .....,
            .....,
         ],
         "ThirdKey": [
            .....,
            .....,
         ]
    ]

    func fetchAllProgress(...) { 
        // do firebase stuff here ...

        // update progressValues
    }

    func updateWeekMap(...) {
        // Uses custom params to map data fetched from firebase to weekMap
    }
}

我们不在 body 中初始化对象。如果模型的生命周期是应用程序的,则它必须是单例;如果模型的生命周期应该与视图相关联,则它必须是 @StateObject。在您的情况下是后者,但是对于这类 loader/fetcher 对象,我们通常不使用 environmentObject 因为通常它们不会在深度视图结构层次结构中共享。

请注意,ObservableObject 是组合框架的一部分,因此如果您的抓取未使用组合,那么您可能想尝试使用较新的 async/await 模式,如果您将其与 SwiftUI 的任务修饰符配对,则您不需要甚至根本不需要对象!