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?
Post not yet marked as solved Up vote post of Xaxxus Down vote post of Xaxxus
2.7k views

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.


  • @fromtoronto But how to pass it the best way? Passing it via a Binding to the init of the UIViewRepresentable struct and then passing this Binding inside the makeCoordinator to the coordinator class?

Add a Comment

MKMapView has different delegate behaviour from normal, it's delegate methods fire when setting its properties. So to workaround that you can disable the delegate when you set the property, e.g.

uiView.delegate = nil
uiView.centerCoordinate = centerCoordinate
uiView.delegate = context.coordinator

You also must update the coordinator's parent so it can access the latest binding (which by the way is just a pair of get and set closures):

context.coordinator.parent = self

Note if you look at PaymentButton.swift in Fruta they set a closure rather than a parent. So if doing it that way you could set the closure nil, set the centerCoordinate then set the closure again.

is there anyone who can provide a functional solution for this issue? I'm having trouble setting the center coordinate anywhere other than in the 'updateUIView' method. This is causing the view to reload and subsequently triggering the delegate to be called again. Than you in advance