UIViewRepresentable:“在视图更新期间修改状态,这将导致未定义的行为”

Bar*_*oth 9 swiftui

我从 MKMapView 制作了一个简单的 UIViewRepresentable。您可以滚动地图视图,屏幕将更新为中间的坐标。

这是内容视图:

import SwiftUI
import CoreLocation

let london = CLLocationCoordinate2D(latitude: 51.50722, longitude: -0.1275)

struct ContentView: View {
    @State private var center = london

    var body: some View {
        VStack {
            MapView(center: self.$center)
            HStack {
                VStack {
                    Text(String(format: "Lat: %.4f", self.center.latitude))
                    Text(String(format: "Long: %.4f", self.center.longitude))
                }
                Spacer()
                Button("Reset") {
                    self.center = london
                }
            }.padding(.horizontal)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是地图视图:

struct MapView: UIViewRepresentable {
    @Binding var center: CLLocationCoordinate2D

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        return mapView
    }

    func updateUIView(_ uiView: MKMapView, context: Context) {
        uiView.centerCoordinate = self.center
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView

        func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
            parent.center = mapView.centerCoordinate
        }

        init(_ parent: MapView) {
            self.parent = parent
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

点击重置按钮只需将 mapView.center 设置为 london 即可。当前方法将使地图滚动速度超慢,并且当点击按钮时,会导致错误“在视图更新期间修改状态,这将导致未定义的行为”。

重置坐标应如何传达给 MKMapView,以便地图滚动再次快速,并修复错误?

Xax*_*xus 9

上述使用 ObservedObject 的解决方案将不起作用。虽然您不会再看到警告消息,但问题仍然存在。Xcode 无法再警告您它的发生。

ObservableObjects 中已发布属性的行为几乎与 @State 和 @Binding 相同。也就是说,只要objectWillUpdate触发发布者,它们就会触发视图更新。当 @Published 属性更新时,这种情况会自动发生。您也可以自己手动触发它objectWillChange.send()

因此,可以使属性不会自动导致视图状态更新。我们可以利用它来防止 UIViewRepresentable 和 UIViewControllerRepresentable 结构的不必要的状态更新。

这是一个当您从 MKMapViewDelegate 方法更新其视图模型时不会循环的实现:

struct MapView: UIViewRepresentable {

    @ObservedObject var viewModel: Self.ViewModel

    func makeUIView(context: Context) -> MKMapView{
        let mapview = MKMapView()
        mapview.delegate = context.coordinator
        return mapview
    }

    func updateUIView(_ uiView: MKMapView, context: Context) {
        // Stop update loop when delegate methods update state.
        guard viewModel.shouldUpdateView else {
            viewModel.shouldUpdateView = true
            return
        }
   
        uiView.centerCoordinate = viewModel.centralCoordinate
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, MKMapViewDelegate {
        private var parent: MapView
    
        init(_ parent: MapView) {
            self.parent = parent
        }
    
        func mapViewDidChangeVisibleRegion(_ mapView: MKMapView){
            // Prevent the below viewModel update from calling itself endlessly.
            parent.viewModel.shouldUpdateView = false
            parent.viewModel.centralCoordinate = mapView.centerCoordinate
        }
    }

    class ViewModel: ObservableObject {
        @Published var centerCoordinate: CLLocationCoordinate2D = .init(latitude: 0, longitude: 0)
        var shouldUpdateView: Bool = true
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您确实不想使用 ObservableObject,另一种方法是将属性shouldUpdateView放入协调器中。尽管我仍然更喜欢使用 viewModel,因为它使您的 UIViewRepresentable 免受多个 @Bindings 的影响。您还可以在外部使用 ViewModel 并通过组合来收听它。

老实说,我很惊讶苹果在创建 UIViewRepresentable 时没有考虑到这个确切的问题。

如果您需要使 SwiftUI 状态与视图更改保持同步,几乎所有 UIKit 视图都会遇到这个问题。