I'm dealing with an app where I have to embed a MKMapView
(UIKit) into a SwiftUI View. Unfortunately I can't use the implemented Map
of SwiftUI, because I have to support clustering (which Map
can't do at the moment).
The problem is, that the MKMapView
doesn't reflect changes applied to the data source (persons
array).
ContentView
struct ContentView: View {
@State private var persons: [Person] = [
.init(
coordinate: .init(latitude: 50.7123012, longitude: 5.1231021),
title: "Person 1"
),
.init(
coordinate: .init(latitude: 49.3123012, longitude: 5.1231021),
title: "Person 2"
)
]
var body: some View {
VStack {
MapView(persons: $persons)
Button("Remove Person at index 0") {
persons.remove(at: 0)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Implementation of MKMapView
with the help of UIViewRepresentable
:
struct MapView: UIViewRepresentable {
@Binding var persons: [Person]
private let mapView = MKMapView()
init(persons: Binding<[Person]>) {
self._persons = persons
// other stuff
}
func makeUIView(context: Context) -> MKMapView {
mapView.delegate = context.coordinator
mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: "testAnnotation")
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
DispatchQueue.main.async {
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(persons)
}
}
}
extension MapView {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "testAnnotation", for: annotation) as? MKMarkerAnnotationView {
annotationView.glyphImage = UIImage(systemName: "person")
return annotationView
}
return nil
}
}
}
Demo Model:
class Person: NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
let title: String?
init(coordinate: CLLocationCoordinate2D, title: String?) {
self.coordinate = coordinate
self.title = title
}
}
Please note, that this is a simplified implementation where I removed logic for clustering, etc. to focus on the problem.
If you tap on "Remove Person at index 0" it actually removes the entry at index 0 but unfortunately the map isn't updated. The problem is that updateUIView
in MapView
doesn't get called and I'm not sure, why this is the case.
Thanks for your help :-)...
CodePudding user response:
Two issues create map view inside makeView
- that's a place for that by design, update annotations right in update (no delay needed) it is a place called on binding update exactly to update UIView counterpart (all UI invalidation/refreshing is made automatically)
So, here is fixed version. Tested with Xcode 13.4 / iOS 15.5
struct MapView: UIViewRepresentable {
@Binding var persons: [Person]
init(persons: Binding<[Person]>) {
self._persons = persons
// other stuff
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView() // << here !!
mapView.delegate = context.coordinator
mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: "testAnnotation")
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.removeAnnotations(mapView.annotations) // << here !!
mapView.addAnnotations(persons)
}
}