当@Published 中的模型更新时 SwiftUI 触发函数 属性
SwiftUI trigger function when model updates inside @Published property
我正在从 Firestore table 获取一个数组。显然是实时的,因此任何新添加的文档或对现有文档的修改都会反映在视图中。这适用于字符串和数字等简单属性,但我也需要显示和刷新其他数据。
我也在存储坐标,如果坐标发生变化,我需要调用 geocoder.reverseGeocodeLocation()
以便从该位置获取地标并将地址显示为字符串。为此,我创建了一个名为 PlacemarkService (code provided below).
的服务
问题是这样的。每当修改模型时,数据都会通过 PackagesViewModel
传递,其中有一个 @Published var results = [Package]()
这是由 Firestore 即时修改的(可编码)。然后,对数组造成的任何更改都会导致刷新 PackagesView
中的 List
,这将导致创建 PackageView(packageService: PackageService(package: package))
的新实例,因为 PackageService
是此处作为参数传递,将创建 PackageService
的新实例。最后,这将触发调用此服务中的 init()。
@Published var results = [Package]() -> List -> PackageView(packageService: PackageService(package: package)) -> init()
所以现在如果我遵循这个流程并在 init 中触发 getPlacemarks()
一切正常(大部分时间),如果包模型发生变化,将在 init 上调用此函数并将 reverseGeocodeLocation 修改后的位置.但是这里有一个大问题,这就是原因。我无法将此逻辑放在 init 上,因为我无法对 for 循环内的数百个位置进行地理编码。我在列表中显示了多个包,而 MapKit 不允许这样做。
So obviously this logic needs to be triggered after the PackageView
is shown, so when one of those packages is selected.
如果我在 PackageView 出现时调用 packageService.getPlacemarks()
这第一次工作正常,但是......当 @Published var results = [Package]()
更新时不会触发 onAppear。
所以最后的问题是:
在哪里调用 packageService.getPlacemarks()
所以只要 @Published var results = [Package]
里面的 Package
更新就会调用它
但出于我上面解释的原因,不在 PlacemarkService 的初始化中。
抱歉解释得太长了,我只是想说清楚。
class PackageService: ObservableObject {
var package: Package
private var cancellables = Set<AnyCancellable>()
@Published var sourcePlacemarkService = PlacemarkService()
@Published var destinationPlacemarkService = PlacemarkService()
var cancellable: AnyCancellable? = nil
init(package: Package) {
self.package = package
startListening()
}
func startListening() {
// Listen to placemark changes.
cancellable = Publishers.CombineLatest(sourcePlacemarkService.$placemark, destinationPlacemarkService.$placemark).sink(receiveValue: {_ in
// Publish changes manually to the view.
self.objectWillChange.send()
})
}
// Get placemarks from locations
func getPlacemarks() {
sourcePlacemarkService.reverseGeocodeLocation(location: package.source.toCLLocation)
destinationPlacemarkService.reverseGeocodeLocation(location: package.destination.toCLLocation)
}
}
class PackagesViewModel: ObservableObject {
@Published var results = [Package]()
func load() {
query.addSnapshotListener { (querySnapshot, error) in
// updates results array when a document is modified.
}
}
}
struct PackagesView: View {
@StateObject var packagessViewModel = PackagesViewModel()
var body: some View {
List(packagessViewModel.results, id: \.self) { package in
NavigationLink(destination: PackageView(packageService: PackageService(package: package))) {
Text(package.title)
}
}
}
}
struct PackageView: View {
@ObservedObject var packageService: PackageService
func onAppear() {
packageService.getPlacemarks()
}
var body: some View {
// show the address from placemark after it is geocoded.
VStack {
Text(packageService.sourcePlacemarkService.placemark.title)
Text(packageService.destinationPlacemarkService.placemark.title)
}
.onAppear(perform: onAppear)
}
}
class PlacemarkService: ObservableObject {
@Published var placemark: CLPlacemark?
init(placemark: CLPlacemark? = nil) {
self.placemark = placemark
}
func reverseGeocodeLocation(location: CLLocation?) {
if let location = location {
geocoder.reverseGeocodeLocation(location, completionHandler: { (placemark, error) in
// some code here
self.placemark = placemark
})
}
}
}
struct Package: Identifiable, Codable {
@DocumentID var id: String?
var documentReference: DocumentReference
var uid: String
var title: String
var description: String
var source, destination: GeoPoint
var amount: Double
var createdAt: Timestamp = Timestamp()
var paid: Bool = false
}
我认为解决问题最简单的方法是先在 onAppear
中调用 getPlacemarks
,然后用 onChange
重新调用它,如下所示:
VStack {
Text(packageService.sourcePlacemarkService.placemark.title)
Text(packageService.destinationPlacemarkService.placemark.title)
}
.onChange(of: packageService.package.id) { _ in
packageService.getPlacemarks()
}
.onAppear {
packageService.getPlacemarks()
}
根据包结构的哪些字段发生变化,您可能需要将其设为 Equtable
并传递整个对象而不是 id
。
首先 - 简化这段代码。大部分代码对于重现问题是不必要的,甚至无法编译。下面的代码 不是 工作代码,而是我们必须在以后开始和更改的代码:
内容视图
struct ContentView: View {
var body: some View {
NavigationView {
PackagesView()
}
}
}
包装服务
class PackageService: ObservableObject {
let package: Package
init(package: Package) {
self.package = package
}
// Get placemarks from locations
func getPlacemarks() {
print("getPlacements called")
}
}
PackagesViewModel
class PackagesViewModel: ObservableObject {
@Published var results = [Package]()
}
包视图
struct PackagesView: View {
@StateObject var packagesViewModel = PackagesViewModel()
var body: some View {
VStack {
Button("Add new package") {
let number = packagesViewModel.results.count + 1
let new = Package(title: "title \(number)", description: "description \(number)")
packagesViewModel.results.append(new)
}
Button("Change random title") {
guard let randomIndex = packagesViewModel.results.indices.randomElement() else {
return
}
packagesViewModel.results[randomIndex].title = "new title (\(Int.random(in: 1 ... 100)))"
}
List(packagesViewModel.results, id: \.self) { package in
NavigationLink(destination: PackageView(packageService: PackageService(package: package))) {
Text(package.title)
}
}
}
}
}
包视图
struct PackageView: View {
@ObservedObject var packageService: PackageService
var body: some View {
VStack {
Text("Title: \(packageService.package.title)")
Text("Description: \(packageService.package.description)")
}
}
}
套餐
struct Package: Identifiable, Hashable {
let id = UUID()
var title: String
let description: String
}
现在,解决问题。我通过检测 results
随 onChange(of:perform:)
的变化解决了这个问题。但是,从这里无法访问视图主体中使用的 PackageService
。
为防止出现此问题,PackageService
实际上存储在 PackagesViewModel
中,这在逻辑上对数据流更有意义。现在 PackageService
也是 struct
,所以 @Published
在 results
的数组上工作,现在可以工作了。
查看下面的代码:
PackageService(更新)
struct PackageService: Hashable {
var package: Package
init(package: Package) {
self.package = package
}
// Get placemarks from locations
mutating func getPlacemarks() {
print("getPlacements called")
// This function is mutating, feel free to set any properties in here
}
}
PackagesViewModel(更新)
class PackagesViewModel: ObservableObject {
@Published var results = [PackageService]()
}
PackagesView(更新)
struct PackagesView: View {
@StateObject var packagesViewModel = PackagesViewModel()
var body: some View {
VStack {
Button("Add new package") {
let number = packagesViewModel.results.count + 1
let new = Package(title: "title \(number)", description: "description \(number)")
packagesViewModel.results.append(PackageService(package: new))
}
Button("Change random title") {
guard let randomIndex = packagesViewModel.results.indices.randomElement() else {
return
}
let newTitle = "new title (\(Int.random(in: 1 ... 100)))"
packagesViewModel.results[randomIndex].package.title = newTitle
}
List($packagesViewModel.results, id: \.self) { $packageService in
NavigationLink(destination: PackageView(packageService: $packageService)) {
Text(packageService.package.title)
}
}
}
.onChange(of: packagesViewModel.results) { _ in
for packageService in $packagesViewModel.results {
packageService.wrappedValue.getPlacemarks()
}
}
}
}
PackageView(更新)
struct PackageView: View {
@Binding var packageService: PackageService
var body: some View {
VStack {
Text("Title: \(packageService.package.title)")
Text("Description: \(packageService.package.description)")
}
}
}
我正在从 Firestore table 获取一个数组。显然是实时的,因此任何新添加的文档或对现有文档的修改都会反映在视图中。这适用于字符串和数字等简单属性,但我也需要显示和刷新其他数据。
我也在存储坐标,如果坐标发生变化,我需要调用 geocoder.reverseGeocodeLocation()
以便从该位置获取地标并将地址显示为字符串。为此,我创建了一个名为 PlacemarkService (code provided below).
问题是这样的。每当修改模型时,数据都会通过 PackagesViewModel
传递,其中有一个 @Published var results = [Package]()
这是由 Firestore 即时修改的(可编码)。然后,对数组造成的任何更改都会导致刷新 PackagesView
中的 List
,这将导致创建 PackageView(packageService: PackageService(package: package))
的新实例,因为 PackageService
是此处作为参数传递,将创建 PackageService
的新实例。最后,这将触发调用此服务中的 init()。
@Published var results = [Package]() -> List -> PackageView(packageService: PackageService(package: package)) -> init()
所以现在如果我遵循这个流程并在 init 中触发 getPlacemarks()
一切正常(大部分时间),如果包模型发生变化,将在 init 上调用此函数并将 reverseGeocodeLocation 修改后的位置.但是这里有一个大问题,这就是原因。我无法将此逻辑放在 init 上,因为我无法对 for 循环内的数百个位置进行地理编码。我在列表中显示了多个包,而 MapKit 不允许这样做。
So obviously this logic needs to be triggered after the PackageView
is shown, so when one of those packages is selected.
如果我在 PackageView 出现时调用 packageService.getPlacemarks()
这第一次工作正常,但是......当 @Published var results = [Package]()
更新时不会触发 onAppear。
所以最后的问题是:
在哪里调用 packageService.getPlacemarks()
所以只要 @Published var results = [Package]
里面的 Package
更新就会调用它
但出于我上面解释的原因,不在 PlacemarkService 的初始化中。
抱歉解释得太长了,我只是想说清楚。
class PackageService: ObservableObject {
var package: Package
private var cancellables = Set<AnyCancellable>()
@Published var sourcePlacemarkService = PlacemarkService()
@Published var destinationPlacemarkService = PlacemarkService()
var cancellable: AnyCancellable? = nil
init(package: Package) {
self.package = package
startListening()
}
func startListening() {
// Listen to placemark changes.
cancellable = Publishers.CombineLatest(sourcePlacemarkService.$placemark, destinationPlacemarkService.$placemark).sink(receiveValue: {_ in
// Publish changes manually to the view.
self.objectWillChange.send()
})
}
// Get placemarks from locations
func getPlacemarks() {
sourcePlacemarkService.reverseGeocodeLocation(location: package.source.toCLLocation)
destinationPlacemarkService.reverseGeocodeLocation(location: package.destination.toCLLocation)
}
}
class PackagesViewModel: ObservableObject {
@Published var results = [Package]()
func load() {
query.addSnapshotListener { (querySnapshot, error) in
// updates results array when a document is modified.
}
}
}
struct PackagesView: View {
@StateObject var packagessViewModel = PackagesViewModel()
var body: some View {
List(packagessViewModel.results, id: \.self) { package in
NavigationLink(destination: PackageView(packageService: PackageService(package: package))) {
Text(package.title)
}
}
}
}
struct PackageView: View {
@ObservedObject var packageService: PackageService
func onAppear() {
packageService.getPlacemarks()
}
var body: some View {
// show the address from placemark after it is geocoded.
VStack {
Text(packageService.sourcePlacemarkService.placemark.title)
Text(packageService.destinationPlacemarkService.placemark.title)
}
.onAppear(perform: onAppear)
}
}
class PlacemarkService: ObservableObject {
@Published var placemark: CLPlacemark?
init(placemark: CLPlacemark? = nil) {
self.placemark = placemark
}
func reverseGeocodeLocation(location: CLLocation?) {
if let location = location {
geocoder.reverseGeocodeLocation(location, completionHandler: { (placemark, error) in
// some code here
self.placemark = placemark
})
}
}
}
struct Package: Identifiable, Codable {
@DocumentID var id: String?
var documentReference: DocumentReference
var uid: String
var title: String
var description: String
var source, destination: GeoPoint
var amount: Double
var createdAt: Timestamp = Timestamp()
var paid: Bool = false
}
我认为解决问题最简单的方法是先在 onAppear
中调用 getPlacemarks
,然后用 onChange
重新调用它,如下所示:
VStack {
Text(packageService.sourcePlacemarkService.placemark.title)
Text(packageService.destinationPlacemarkService.placemark.title)
}
.onChange(of: packageService.package.id) { _ in
packageService.getPlacemarks()
}
.onAppear {
packageService.getPlacemarks()
}
根据包结构的哪些字段发生变化,您可能需要将其设为 Equtable
并传递整个对象而不是 id
。
首先 - 简化这段代码。大部分代码对于重现问题是不必要的,甚至无法编译。下面的代码 不是 工作代码,而是我们必须在以后开始和更改的代码:
内容视图
struct ContentView: View {
var body: some View {
NavigationView {
PackagesView()
}
}
}
包装服务
class PackageService: ObservableObject {
let package: Package
init(package: Package) {
self.package = package
}
// Get placemarks from locations
func getPlacemarks() {
print("getPlacements called")
}
}
PackagesViewModel
class PackagesViewModel: ObservableObject {
@Published var results = [Package]()
}
包视图
struct PackagesView: View {
@StateObject var packagesViewModel = PackagesViewModel()
var body: some View {
VStack {
Button("Add new package") {
let number = packagesViewModel.results.count + 1
let new = Package(title: "title \(number)", description: "description \(number)")
packagesViewModel.results.append(new)
}
Button("Change random title") {
guard let randomIndex = packagesViewModel.results.indices.randomElement() else {
return
}
packagesViewModel.results[randomIndex].title = "new title (\(Int.random(in: 1 ... 100)))"
}
List(packagesViewModel.results, id: \.self) { package in
NavigationLink(destination: PackageView(packageService: PackageService(package: package))) {
Text(package.title)
}
}
}
}
}
包视图
struct PackageView: View {
@ObservedObject var packageService: PackageService
var body: some View {
VStack {
Text("Title: \(packageService.package.title)")
Text("Description: \(packageService.package.description)")
}
}
}
套餐
struct Package: Identifiable, Hashable {
let id = UUID()
var title: String
let description: String
}
现在,解决问题。我通过检测 results
随 onChange(of:perform:)
的变化解决了这个问题。但是,从这里无法访问视图主体中使用的 PackageService
。
为防止出现此问题,PackageService
实际上存储在 PackagesViewModel
中,这在逻辑上对数据流更有意义。现在 PackageService
也是 struct
,所以 @Published
在 results
的数组上工作,现在可以工作了。
查看下面的代码:
PackageService(更新)
struct PackageService: Hashable {
var package: Package
init(package: Package) {
self.package = package
}
// Get placemarks from locations
mutating func getPlacemarks() {
print("getPlacements called")
// This function is mutating, feel free to set any properties in here
}
}
PackagesViewModel(更新)
class PackagesViewModel: ObservableObject {
@Published var results = [PackageService]()
}
PackagesView(更新)
struct PackagesView: View {
@StateObject var packagesViewModel = PackagesViewModel()
var body: some View {
VStack {
Button("Add new package") {
let number = packagesViewModel.results.count + 1
let new = Package(title: "title \(number)", description: "description \(number)")
packagesViewModel.results.append(PackageService(package: new))
}
Button("Change random title") {
guard let randomIndex = packagesViewModel.results.indices.randomElement() else {
return
}
let newTitle = "new title (\(Int.random(in: 1 ... 100)))"
packagesViewModel.results[randomIndex].package.title = newTitle
}
List($packagesViewModel.results, id: \.self) { $packageService in
NavigationLink(destination: PackageView(packageService: $packageService)) {
Text(packageService.package.title)
}
}
}
.onChange(of: packagesViewModel.results) { _ in
for packageService in $packagesViewModel.results {
packageService.wrappedValue.getPlacemarks()
}
}
}
}
PackageView(更新)
struct PackageView: View {
@Binding var packageService: PackageService
var body: some View {
VStack {
Text("Title: \(packageService.package.title)")
Text("Description: \(packageService.package.description)")
}
}
}