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

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

  1. 第一个视图称为 MapView,其中包含一个带有不同注释的地图。
  2. 第二个视图称为 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

标签: ios swift firebase swiftui mapkit


【解决方案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 更新很有用。

【讨论】:

  • 这是一个好点 - 但肯定会弹出诸如“不允许从后台线程发布更改”之类的警告吗?地图仍在更新(至少对我而言)。
猜你喜欢
  • 2020-03-18
  • 2020-03-21
  • 1970-01-01
  • 1970-01-01
  • 2022-01-12
  • 2021-10-15
  • 2018-02-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多