UIViewRepresentable creating new instance on state change

142 Views Asked by At

I'm new to Swift/UIKit and am trying to implement a MapLibreGL map that displays a list of markers. As the user changes search criteria, the list of markers changes, and I'm struggling to update the markers when this changes.

The challenge I'm facing is that when my state changes, the MapView UIViewRepresentable seems to be reinitialized, resulting in a new mapView object (MGLMapView).

On state change, updateUIView is called, but makeUIView and the coordiniator's init aren't called again (they're only called on first load).

When I then try to add/remove annotations to mapView, it's referencing the wrong mapView (the new one, that I don't believe is actually being displayed), and my view isn't updating.


struct MapView: UIViewRepresentable {
    var mapView = MGLMapView(frame: .zero) //this object is recreated when my state changes
    var symbols: Binding<[MapMarkerSymbol]>?
    func updateUIView(_ uiView: MGLMapView, context: Context) {
        print("*****UPDATING******")
        //        This gets hit on state change
       
        //        let x = uiView.annotations;
        //        let y = mapView.annotations;
        // on intial load uiView and mapView reference the same object
        //on future state changes, they reference different objects
        
        context.coordinator.reload()
    }

     func makeUIView(context: Context) -> MGLMapView {
        print("****MAKING***")
        //      This only gets hit on first load
        let styleURL = getStyleURL()
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.logoView.isHidden = true
        mapView.styleURL = styleURL
        mapView.delegate = context.coordinator
        return mapView
    }
    func createMarkerSymbol(symbol: MapMarkerSymbol) {
        let annotation = MGLPointAnnotation()
        annotation.coordinate = CLLocationCoordinate2D(latitude: symbol.lat, longitude: symbol.lng)
        annotation.title = symbol.title
        annotation.subtitle = ""
        mapView.addAnnotation(annotation)
    }
    final class Coordinator: NSObject, MGLMapViewDelegate {
        var control: MapView
        
        init(_ control: MapView) {
            self.control = control
        }
        func reload(){
            //this should remove the old annotations, and add a new set.
            //On state changes, the correct new set is being passed, but the mapView reference 
            //I get here is a new one, not the original one (that's being displayed), so it 
            //has no annotations to remove, and adding new ones to it doesn't impact the UI
           
            control.mapView.removeAnnotations(control.mapView.annotations ?? [])
            if let symbols = control.symbols{
                for symbol in symbols.wrappedValue {
                    control.createMarkerSymbol(symbol: symbol)
                }
            }
        }
        func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
            
            print("*** DID FINISH LOADING *****")
            
            if !(control.symbols?.isEmpty ?? true) {
                control.zoomToBounds()
            }
            
        }
        func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
            // Always allow callouts to popup when annotations are tapped.
            return true
        }
        func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
            print("annotation tapped")
        }
        
    }
    
    
    func makeCoordinator() -> MapView.Coordinator {
        Coordinator(self)
    }

I have tried with my "symbols" as a binding value and just a list, but it doesn't make a difference.

Here's a snipped of where this is being used:

    @StateObject private var viewModel = IOSFindBusinessesViewModel()
      var body: some View {
        @State var state = viewModel.state
       
            ZStack{
                if(state.isShowingMapLayout){
                    let businessPins = state.finalResultsToDisplay().map{business in
                        MapMarkerSymbol(lat: business.lat, lng: business.lng, title: business.name)
                    }
                        MapView(mapFiles: state.mapFiles, symbols: Binding(get: {
                            businessPins
                        }, set: { _ in
                            
                        }) )
                            
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                    }
                
    }

What am I doing wrong? Why is the UIViewRepresentable being "partially recreated" - the mapView object is new, but the mapView that's being displayed (and the uiView provided in updateUIView) are the original views that were first created.

How can I maintain a reference to that original mapView so I can interact with it elsewhere?

0

There are 0 best solutions below