swiftswiftuimapkitmapkitannotation

Display overlay view when selecting annotation in SwiftUI


I'm using UIRepresentable to show annotations on a map, and want to be able to show a view when tapping on the selected pin.

I was previously using Map() so was able to use the .onTapGesture for the annotations, but now that the annotations are made from UIKit, how to I pass the selected item to the main view?

What I previously had working:

var body: some View {
  ZStack {
    Map(region: $region, annotationItems: $model.locations) { location in
      MapPin(coordinate: location.coord)
        .onTapGesture {
          modelData.selectedLocation = location
          modelData.isShowingDetail = true
        }
    }
    if modelData.isShowingDetail {
      DetailView(
        isShowingDetail: $modelData.isShowingDetail,
        location: modelData.selectedLocation!
      )
    }
  }
}

Now I have the UIViewRepresentable:

struct UIMapView: UIViewRepresentable {

  // default setup - coordinator, makeUI, updateUI

  class Coordinator: NSObject, MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
      // how to trigger the overlay??
    }
  }
}

Any help would be appreciated as I am very stuck on this :)


Solution

  • You want to know the selected annotation in your SwiftUI view. So you have to store it somewhere. Declare a @State :

    struct ContentView: View {
        let locations: [MKAnnotation]
        @State private var selectedLocation: MKAnnotation?
        var body: some View {
                // ... //
        }
    }
    

    Now in your wrapper (UIViewRepresentable) you have to make a binding with this MKAnnotation? :

    struct MapView: UIViewRepresentable {
        @Binding var selectedLocation: MKAnnotation?    // HERE
        let annotations: [MKAnnotation]
    
        func makeUIView(context: Context) -> MKMapView {
            let mapView = MKMapView()
            mapView.region = // .... //
            mapView.addAnnotations(annotations)
            mapView.delegate = context.coordinator
            return mapView
        }
    
        func updateUIView(_ view: MKMapView, context: Context) {
            // .... //
        }
    

    Now you should be able to access this variable in your Delegate (Coordinator). For that you have to pass the UIViewRepresentable to the Coordinator:

        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
    
        class Coordinator: NSObject, MKMapViewDelegate {
            var parent: MapView
    
            init(_ parent: MapView) {
                self.parent = parent
            }
    

    And finally in func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) you can copy the MKAnnotation in parent.selectedLocation . With the @Binding this MKAnnotation is now accessible in your parent view (ContentView). You can display its properties in your DetailView.

            func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
                // ... //
            }
            
            func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
                parent.selectedLocation = view.annotation
            }
        }
    }
    

    For example :

    struct ContentView: View {
        let locations: [MKAnnotation]
        @State private var selectedLocation: MKAnnotation?
        var body: some View {
            VStack {
                Text("\(selectedLocation?.coordinate.latitude ?? 0)")
    
                // Don't forget the Binding : $selectedLocation
    
                MapView(selectedLocation: $selectedLocation, annotations: locations)
            }
        }
    }