ObservedObject 不会注意到 ObservableObject 数组的变化
ObservedObject does not notice changes of ObservableObject Array
我在将 ObservedObject 与 SwiftUI 结合使用时遇到问题。我的应用程序有两个主要视图:
- 第一个视图称为 MapView,里面有一张带有不同注释的地图。
- 第二个视图称为 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()
它都是一个不同的实例,并且一个人不知道另一个人在做什么。
其次,您在没有 @StateObject
的 View
中创建 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()
}
}
我在将 ObservedObject 与 SwiftUI 结合使用时遇到问题。我的应用程序有两个主要视图:
- 第一个视图称为 MapView,里面有一张带有不同注释的地图。
- 第二个视图称为 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()
它都是一个不同的实例,并且一个人不知道另一个人在做什么。
其次,您在没有 @StateObject
的 View
中创建 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()
}
}