ObservedObject 不会注意到 ObservableObject 数组的变化

ObservedObject does not notice changes of ObservableObject Array

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

  1. 第一个视图称为 MapView,里面有一张带有不同注释的地图。
  2. 第二个视图称为 MapSettingsView,它有一个 DatePicker 和一个类别选择器,我可以在其中决定 MapView 中的哪些注释可见,哪些不可见。

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

在 MapSettingsView 中选择我想要的所有属性后,我有一个按钮,它调用我的 FirebaseTasks 中的一个函数 (loadPublicLocations) Class。然后,此函数会根据来自 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)
        }        
    }    
}

FirebaseTasks:

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



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

用于位置服务的 PublicLocationApi 等 API 在后台线程而不是主线程上调用完成处理程序是很常见的。我猜这就是这里发生的事情。要在主线程上进行 UI 更新,请替换

self.publicLocations.append(location)

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

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

一些可能性,因为你没有提供 MapSettingsView 我无法给出明确的原因

首先,很可能是根据您如何称呼您的 .sheet 内容。

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

其次,您在没有 @StateObjectView 中创建 ObservableObject。以下两行都不安全,可能会导致 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()
    }
}