【问题标题】:SwiftUI trigger function when model updates inside @Published property@Published 属性内模型更新时的 SwiftUI 触发功能
【发布时间】:2021-12-27 03:22:47
【问题描述】:

我从 Firestore 表中获取一个数组。显然是实时的,因此任何新添加的文档或对现有文档的修改都会反映在视图中。这适用于字符串和数字等简单属性,但我也需要显示和刷新其他数据。

我也在存储坐标,如果坐标发生变化,我需要调用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 上调用,并将反转修改位置的 GeocodeLocation。但是这里有一个大问题,这就是原因。我无法将此逻辑放在 init 上,因为我无法对 for 循环内的数百个位置进行地理编码。我在一个列表中显示了多个包,而 MapKit 不允许这样做。

显然,在显示PackageView之后需要触发此逻辑,因此选择其中一个包。

如果我在 PackageView 出现时调用 packageService.getPlacemarks(),这第一次可以正常工作,但是......当 @Published var results = [Package]() 更新时不会触发 onAppear。

所以最后的问题是:

在哪里调用 packageService.getPlacemarks(),以便在 @Published var results = [Package] 中的 Package 更新时调用它

但由于我上面解释的原因,不在 PlacemarkService 的 init 上。

对不起,解释太长了,我只是想清楚一点。

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
}

【问题讨论】:

  • 把它放在didSet 中换成results

标签: ios swift swiftui swift5 combine


【解决方案1】:

首先 - 简化此代码。大部分代码对于重现问题是不必要的,甚至无法编译。下面的代码不是可以工作的代码,而是我们以后必须启动和更改的代码:

内容视图

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:) 的变化来解决此问题。但是,从这里无法访问视图正文中使用的PackageServices。

为了防止这个问题,PackageServices 实际上存储在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)")
        }
    }
}

【讨论】:

  • .onChange(of: packagesViewModel.results) { _ in for packageService in $packagesViewModel.results { packageService.wrappedValue.getPlacemarks() } } 这是一个异步调用,这意味着每次更改都会触发多次。这是放置这个的最佳位置吗?这不应该在 PackageView 中吗?
  • 在问题中:`我无法在 for 循环中对数百个位置进行地理编码。我在一个列表中显示了多个包,而 MapKit 不允许这样做。`
  • @M1X 这取决于你在寻找什么。如果您在PackageService 中更改package,这会触发PackagesView 中的更新,它可能会多次调用onChange。但是,这是意料之中的,因为在更改包时视图会正确更新。您可能想要做的是onChange 中的if 中的一个条件,仅在特定属性实际更改时触发。
  • @M1X 你可以做.onChange(of: packagesViewModel.results) { [old = packagesViewModel.results] new in ... },并且只有在某些属性发生变化时才允许调用getPlacemarks()
  • .onChange(of: packageService) { _ in $packageService.wrappedValue.refreshPlacemarks() } 检测PackageView中的变化怎么样..我试过这个它编译但视图没有刷新
【解决方案2】:

我认为解决您的问题的最简单方法是首先在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

【讨论】:

  • 我收到一个错误:Value of type 'Package' has no member 'objectWillChange'。这将刷新视图还是更新数据?因为更新数据不是它已经工作的问题,但正如我提到的,当package.source 更改时,我需要触发sourcePlacemarkService.reverseGeocodeLocation(location: package.source.toCLLocation)
  • @M1X 我第一次没有仔细阅读你的问题,请看更新的答案。
  • 我创建了 PlaceMark 服务的新实例,但未调用 getPlacemarks()
猜你喜欢
  • 2021-03-23
  • 2022-01-04
  • 1970-01-01
  • 2020-10-18
  • 2020-11-12
  • 2021-04-01
  • 2020-04-30
  • 2020-10-21
  • 1970-01-01
相关资源
最近更新 更多