State loops in UIViewRepresentable

I have been working with UIViewRepresentable a lot recently and noticed something that seems to be quite the flaw:

There doesn't seem to be a clean way of passing data back to your swiftUI views in a performant way.

Here is an example:

Code Block
struct MapWrapper: UIViewRepresentable {
@Binding var centerCoordinate: CLLocationCoordinate2D
init(centerCoordinate: Binding<CLLocationCoordinate2D>) {
self._centerCoordinate = centerCoordinate
}
func makeUIView(context: Context) -> MKMapView {
var mapView = MKMapView()
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(uiView: MKMapView, context: Context) {
// Updating the maps center coordinate triggers mapViewDidChangeVisibleRegion, which thus causes updateUIView to trigger again.
uiView.centerCoordinate = centerCoordinate
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: MKMapViewDelegate {
var parent: MapWrapper
init(parent: MapWrapper) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
// updating this variable causes a view reload, thus calling updateUIView, and eventually causing this delegate method to trigger again.
parent.centerCoordinate = mapView.centerCoordinate
}
}


As you can see from the above code, dragging the map anywhere would cause a loop.

The only workaround I have found for this is to give your coordinator a "shouldUpdateState" Boolean variable. and set that prior to updating your bindings.

Has this ever been addressed by apple anywhere? Or are we just expected to only modify our view state from the outside of a UIViewRepresentable?

Replies

Don't pass the SwiftUI View to the coordinator. It is a struct and many copies of it are made as the screen is redrawn. Instead put the center location in an ObservableObject pass that and additionally, if you need it, a reference to the UIView itself (it is an object and only one copy is created. In your case you don't need use it because the delegate gives you the reference to the UIView, but often you do need your own reference).

So make something like
class MapState: ObservableObject { @Published var centre: CLLocationCoordiate2D? }. Have the SwiftUI View hold that, either as StateObject, ObservedObject or EnvironmentObject depending on where the object is made.
Pass a reference of that object to the Coordinator.
When the delegate calls, update that property and the @Published should send that back to the SwiftUI view to update.