Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Accessing MKMapView elements as UIViewRepresentable in the main (ContentView) SwiftUI view

I am using SwiftUI to display a map and if user tapped on an annotation, it pops up a detail view in the VStack. I have made the map view and inserted annotations in another SwiftUI file. I also made the detail view.

How can I access the annotations of that map in the main view file to define a .tapaction for them to use it for the detailed view?

I tried defining the view as MKMapView but it is not possible to do it for a UIViewRepresentable inside another SwiftUI view.

The main view (ContentView) code is:

struct ContentView: View {
    @State private var chosen = false

    var body: some View {

        VStack {
            MapView()
            .edgesIgnoringSafeArea(.top)
            .frame(height: chosen ? 600:nil)
            .tapAction {
            withAnimation{ self.chosen.toggle()}
            }

    if chosen {
        ExtractedView()
            }
        }
    }
}

The MapView code is:

struct MapView : UIViewRepresentable {
    @State private var userLocationIsEnabled = false
    var locationManager = CLLocationManager()
    func makeUIView(context: Context) -> MKMapView {
    MKMapView(frame: .zero)

    }
    func updateUIView(_ view: MKMapView, context: Context) {

        view.showsUserLocation = true

        .
        .
        .

            let sampleCoordinates = [
                CLLocation(latitude: xx.xxx, longitude: xx.xxx),
                CLLocation(latitude: xx.xxx, longitude: xx.xxx),
                CLLocation(latitude: xx.xxx, longitude: xx.xxx)
                ]
            addAnnotations(coords: sampleCoordinates, view: view)

        }
    }

}

I expect to be able to access map view annotations and define tapaction in another view.

like image 585
MRF Avatar asked Jun 12 '19 13:06

MRF


1 Answers

In SwiftUI DSL you don't access views.

Instead, you combine "representations" of them to create views.

A pin can be represented by an object - manipulating the pin will also update the map.

This is our pin object:

class MapPin: NSObject, MKAnnotation {

    let coordinate: CLLocationCoordinate2D
    let title: String?
    let subtitle: String?
    let action: (() -> Void)?

    init(coordinate: CLLocationCoordinate2D,
         title: String? = nil,
         subtitle: String? = nil,
         action: (() -> Void)? = nil) {
        self.coordinate = coordinate
        self.title = title
        self.subtitle = subtitle
        self.action = action
    }

}

Here's my Map, which is not just UIViewRepresentable, but also makes use of a Coordinator.

(More about UIViewRepresentable and coordinators can be found in the excellent WWDC 2019 talk - Integrating SwiftUI)

struct Map : UIViewRepresentable {

    class Coordinator: NSObject, MKMapViewDelegate {

        @Binding var selectedPin: MapPin?

        init(selectedPin: Binding<MapPin?>) {
            _selectedPin = selectedPin
        }

        func mapView(_ mapView: MKMapView,
                     didSelect view: MKAnnotationView) {
            guard let pin = view.annotation as? MapPin else {
                return
            }
            pin.action?()
            selectedPin = pin
        }

        func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
            guard (view.annotation as? MapPin) != nil else {
                return
            }
            selectedPin = nil
        }
    }

    @Binding var pins: [MapPin]
    @Binding var selectedPin: MapPin?

    func makeCoordinator() -> Coordinator {
        return Coordinator(selectedPin: $selectedPin)
    }

    func makeUIView(context: Context) -> MKMapView {
        let view = MKMapView(frame: .zero)
        view.delegate = context.coordinator
        return view
    }

    func updateUIView(_ uiView: MKMapView, context: Context) {

        uiView.removeAnnotations(uiView.annotations)
        uiView.addAnnotations(pins)
        if let selectedPin = selectedPin {
            uiView.selectAnnotation(selectedPin, animated: false)
        }

    }

}

The idea is:

  • The pins are a @State on the view containing the map, and are passed down as a binding.
  • Each time a pin is added or removed, it will trigger a UI update - all the pins will be removed, then added again (not very efficient, but that's beyond the scope of this answer)
  • The Coordinator is the map delegate - I can retrieve the touched MapPin from the delegate methods.

To test it:

struct ContentView: View {

    @State var pins: [MapPin] = [
        MapPin(coordinate: CLLocationCoordinate2D(latitude: 51.509865,
                                                  longitude: -0.118092),
               title: "London",
               subtitle: "Big Smoke",
               action: { print("Hey mate!") } )
    ]
    @State var selectedPin: MapPin?

    var body: some View {
        NavigationView {
            VStack {
                Map(pins: $pins, selectedPin: $selectedPin)
                    .frame(width: 300, height: 300)
                if selectedPin != nil {
                    Text(verbatim: "Welcome to \(selectedPin?.title ?? "???")!")
                }
            }
        }

    }

}

...and try zooming/tapping the pin on London, UK :)

enter image description here

like image 187
Matteo Pacini Avatar answered Nov 15 '22 23:11

Matteo Pacini