ObservedObject 不会注意到 ObservableObject Array 的变化

Posted

技术标签:

【中文标题】ObservedObject 不会注意到 ObservableObject Array 的变化【英文标题】:ObservedObject does not notice changes of ObservableObject Array 【发布时间】:2021-08-02 18:22:10 【问题描述】:

我在将 ObservedObject 与 SwiftUI 一起使用时遇到问题。我的应用有两个主要视图:

    第一个视图称为 MapView,其中包含一个带有不同注释的地图。 第二个视图称为 MapSettingsView,有一个 DatePicker 和一个用于类别的 Picker,我可以在其中决定 MapView 中哪些注释可见,哪些不可见。

每个注释都有特定的属性,例如日期、类别和坐标(纬度、经度)。 每个注释都保存在我的 Firebase 实时数据库中,因此为了获取它们,我构建了一个 API,它可以调用我想要的所有信息,效果很好。

在 MapSettingsView 中选择了我想要的所有属性后,我有一个按钮,它在我的 FirebaseTasks 类中调用一个函数 (loadPublicLocations)。然后,此函数根据 Firebase 中的这些属性调用所有位置。这也很有效,因为我打印了所有想要的位置。为了根据新的位置数组刷新 MapView,FirebaseTasks 中的所有位置都附加到 @Published var publicLocations = [LocationModel]()。在 MapView 内部,注释基于这个数组,我通过 @ObservedObject 属性获得。

现在的问题是:在 FirebaseTasks 中,所有想要的位置都附加到“publicLocations”数组中。 MapView 不会刷新,虽然注解是基于 ObservedObject taskModel.publicLocations

地图视图:

struct MapView: View 
    
    @ObservedObject var taskModel = FirebaseTasks() 
    @State var showSettings = false
 
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(
            latitude: 25.7617,
            longitude: 80.1918
        ),
        span: MKCoordinateSpan(
            latitudeDelta: 10,
            longitudeDelta: 10
        )
    )

    var body: some View 
        
        VStack 
            HStack 
                Button(action: 
                    showSettings.toggle()

                ) 
                    Image(systemName: "line.horizontal.3.decrease")
                        .font(.title)
                        .foregroundColor(.black)
                
                .sheet(isPresented: $showSettings) 
                    MapSettings(mapViewModel: MapSettingsViewModel("all", category: "all", lat: 21, long: 21))
                                
                                        
            
            .padding(.horizontal)
            .padding(.top)
            .padding(.bottom,10)
            

            Map(coordinateRegion: $region, annotationItems: taskModel.publicLocations, annotationContent:  (location) -> MapMarker in    
                MapMarker(coordinate: CLLocationCoordinate2D(latitude: location.lat!, longitude: location.long!), tint: .red) // does not get data on refreshed MapSettings
            )
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                .edgesIgnoringSafeArea(.all)
                
        

Firebase 任务:

class FirebaseTasks: ObservableObject 
    
    // Model
    @Published var publicLocations = [LocationModel]() 
        didSet 
            print(publicLocations) // works
        
      
    
    // Location related
    func loadPublicLocations(category: String, date: String, currentLat: Double, currentLong: Double) 
        publicLocations.removeAll()

        PublicLocationApi.system.addPublicRadiusLocationObserver(category, date: date, currentLat: currentLat, currentLong: currentLong)  (location) in
            print(location) // works
            self.publicLocations.append(location)           
        
     




【问题讨论】:

在我稍微简化了这段代码以便我可以运行它之后,它非常适合我。问题可能在于您未显示的代码,例如LocationModel 【参考方案1】:

由于您没有提供MapSettingsView,所以有些可能性我无法给出明确的原因

首先,很可能取决于您如何称呼您的 .sheet 内容。

您正在创建 FirebaseTasks() 的单独实例,每次调用 FirebaseTasks() 时,它都是不同的实例,一个不知道另一个在做什么。

其次,您在没有@StateObjectView 中创建ObservableObjects。下面的两行都是不安全的,当View 重新加载时,可能会导致您的所有模型数据丢失。

@ObservedObject var taskModel = FirebaseTasks() 

MapSettings(mapViewModel: MapSettingsViewModel("all", category: "all", lat: 21, long: 21))

https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

import SwiftUI
import MapKit
struct ChainedMapView: View 
    //@ObservedObject is used when the Observable Object is coming from the previous View or a class
    @StateObject var taskModel = FirebaseTasks()
    @State var showSettings = false
    
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(
            latitude: 25.7617,
            longitude: 80.1918
        ),
        span: MKCoordinateSpan(
            latitudeDelta: 10,
            longitudeDelta: 10
        )
    )
    var body: some View 
        VStack 
            HStack 
                Button(action: 
                    showSettings.toggle()
                    
                ) 
                    Image(systemName: "line.horizontal.3.decrease")
                        .font(.title)
                        .foregroundColor(.black)
                
                //Creating Observable Objects in a View without StateObject is considered unsafe
                //https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
                .sheet(isPresented: $showSettings) 
                    //Your load should be related to the region so it is visible
                    MapSettingsView(category: "all", lat: region.center.latitude, long: region.center.longitude)
                    //You have to pass the same instance of the ObservableObject
                        .environmentObject(taskModel)
                    
                
            
            .padding(.horizontal)
            .padding(.top)
            .padding(.bottom,10)
            
            Map(coordinateRegion: $region, annotationItems: taskModel.publicLocations, annotationContent:  (location) -> MapMarker in
                MapMarker(coordinate: location.coordinates, tint: .red)
            )
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                .edgesIgnoringSafeArea(.all)
        
    

class FirebaseTasks: ObservableObject 
    // Model
    @Published var publicLocations = [LocationModel]() 
        didSet 
            print(publicLocations) // works
        
    
    // Location related
    func loadPublicLocations(category: String, date: String, currentLat: Double, currentLong: Double) 
        publicLocations.removeAll()
        
        //PublicLocationApi.system.addPublicRadiusLocationObserver(category, date: date, currentLat: currentLat, currentLong: currentLong)  (location) in
        
        //Created random objects since I dont have access to your API
        for _ in 0...10
            let location = LocationModel(coordinates: CLLocationCoordinate2D(latitude: Double.random(in: currentLat...currentLat+1), longitude: Double.random(in: currentLong...currentLong+1)), date: Date())
            print(location) // works
            self.publicLocations.append(location)
        
    


struct LocationModel: Identifiable
    let id: UUID = UUID()
    var coordinates: CLLocationCoordinate2D
    var date: Date

struct MapSettingsView: View
    //If you are doing something like this you are creating a seperate instance
    //One does not know what the other is doing
    //@StateObject var taskModel = FirebaseTasks()
    
    //This is the shared Object
    @EnvironmentObject var taskModel: FirebaseTasks
    
    //You have not described how you use this
    //@StateObject var vm: MapSettingsViewModel = MapSettingsViewModel()
    let category: String
    let lat: Double
    let long: Double
    var body: some View 
        Button("load", action: 
            taskModel.loadPublicLocations(category: category, date: Date().description, currentLat: lat, currentLong: long)
        ).onAppear(perform: 
            //To put initial values into your ViewModel it is best to do it here.
            //Dont Create the ObservableObject in the body of a View
        )
        
    


struct ChainedMapView_Previews: PreviewProvider 
    static var previews: some View 
        ChainedMapView()
    

【讨论】:

非常感谢,这是我的错。我没有为 MapSettings 中的 taskModel 和 MapView 中的属性 @StateObject 使用共享属性 @EnvironmentObject。这解决了我的问题。【参考方案2】:

在 SwiftUI 中触发 UI 更新的任何代码都必须在 main thread 上运行。如果 ObservableObject 内的代码更改了另一个线程上的 @Published 属性,则更改可能不会反映在 UI 中(即使它会正确触发 didSet 内的代码,这就是它起作用的原因)。

您用于定位服务的 API(例如 PublicLocationApi)在后台线程而不是主线程上调用完成处理程序是很常见的。我的猜测是这里正在发生的事情。要在主线程上更新 UI,请替换

self.publicLocations.append(location)

DispatchQueue.main.async 
    self.publicLocations.append(location)

DispatchQueue.main.async 函数在主线程上运行——传递给它的闭包中的代码,因此它对于进行 UI 更新很有用。

【讨论】:

这是一个好点 - 但肯定会弹出诸如“不允许从后台线程发布更改”之类的警告吗?地图仍在更新(至少对我而言)。

以上是关于ObservedObject 不会注意到 ObservableObject Array 的变化的主要内容,如果未能解决你的问题,请参考以下文章

当从另一个 ObservedObject 链接到 Published 属性时,视图不会对更改做出反应

SwiftUI - ObservedObject 永远不会被释放

嵌套 ObservedObject 中的更改不会更新 UI

SwiftUI - 尽管使用了 @Published 和 @ObservedObject,但视图不会更新

SwiftUI:如果 @ObservedObject 是 UIViewController 的子类,则不会重新加载视图内容。这是一个错误还是我错过了啥?

SwiftUI - 从 Swift 类启动的可观察对象不会更新 ContentView() 上的 @ObservedObject