当@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
}

现在,解决问题。我通过检测 resultsonChange(of:perform:) 的变化解决了这个问题。但是,从这里无法访问视图主体中使用的 PackageService

为防止出现此问题,PackageService 实际上存储在 PackagesViewModel 中,这在逻辑上对数据流更有意义。现在 PackageService 也是 struct,所以 @Publishedresults 的数组上工作,现在可以工作了。

查看下面的代码:

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