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?