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

时间:2020-01-30 08:04:04

标签: swiftui

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

这是ContentView:

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

这是MapView:

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

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

应如何将坐标重置传递给MKMapView,以使地图再次快速滚动且错误得到修复?

2 个答案:

答案 0 :(得分:1)

上述带有 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
    }
}

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

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

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

答案 1 :(得分:0)

如果将中心变量放在可观察对象中,则速度问题似乎已解决。它消除了可能导致延迟的异步更新需求。

import SwiftUI
import CoreLocation
import MapKit

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

class MapController: ObservableObject{
    @Published var center: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 51.50722, longitude: -0.1275)
    func resetCenter(){
        center = CLLocationCoordinate2D(latitude: 51.50722, longitude: -0.1275)
    }
}
struct MapViewCenter: View {
    //@State private var center = london
    @ObservedObject var mapCont: MapController = MapController()
    @State var refresh: Bool = false
    var body: some View {
        VStack {
            MapView(refresh: $refresh, mapCont: mapCont)
            HStack {
                VStack {
                    Text(String(format: "Lat: %.4f", self.mapCont.center.latitude))
                    Text(String(format: "Long: %.4f", self.mapCont.center.longitude))
                }
                Spacer()
                Button("Reset") {
                    print("resetButton")
                    self.mapCont.resetCenter()
                    self.refresh.toggle()
                    print("resetButton :: center = \(self.mapCont.center)")
                }
            }.padding(.horizontal)
        }
    }
}
struct MapView: UIViewRepresentable {
    @Binding var refresh: Bool
    var mapCont: MapController

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

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

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

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView
        func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
            parent.mapCont.center = mapView.centerCoordinate
        }

        init(_ parent: MapView) {
            self.parent = parent
        }
    }
}