2

I have a SwiftUI app with SwiftUI life cycle and I'm trying to get a tap gesture to work in an MKMapView. I can't seem to get the reference to the map correctly. All I want to do in this subsection is tap the map and add an annotation. I would then also use the coordinates of the annotation.

Here's the code.

struct MapView: UIViewRepresentable {

    typealias UIViewType = MKMapView
    @State private var myMapView: MKMapView?

    class Coordinator: NSObject, MKMapViewDelegate {
        var control: MapView
        let parent = MapView()
    
        let sfCoord = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)

        init(_ control: MapView) {
            self.control = control
        }
    
        func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
            if let annotationView = views.first {
                if let annotation = annotationView.annotation {
                    if annotation is MKUserLocation {
                        let region = MKCoordinateRegion(center: annotation.coordinate, latitudinalMeters: 2000, longitudinalMeters: 2000)
                        mapView.setRegion(region, animated: true)
                    }
                }
            }
        }//did add
    
        @objc func addAnnotationOnTapGesture(sender: UITapGestureRecognizer) {
            if sender.state == .ended {
                print("in addAnnotationOnTapGesture")
                let point = sender.location(in: parent.myMapView)
                print("point is \(point)")
                let coordinate = parent.myMapView?.convert(point, toCoordinateFrom: parent.myMapView)
                print("coordinate?.latitude is \(String(describing: coordinate?.latitude))")
                let annotation = MKPointAnnotation()
                annotation.coordinate = coordinate ?? sfCoord
                annotation.title = "Start"
                parent.myMapView?.addAnnotation(annotation)
            }
        }
    }//coord

    func makeUIView(context: Context) -> MKMapView {
        let map = MKMapView()
        map.showsUserLocation = true
        map.delegate = context.coordinator
    
        DispatchQueue.main.async {
            self.myMapView = map
        }
    
        let gRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.addAnnotationOnTapGesture(sender:)))
        map.addGestureRecognizer(gRecognizer)

        return map
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

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

    }
}//struct MapView

And the LocationManager for reference:

class LocationManager: NSObject, ObservableObject {
    private let locationManager = CLLocationManager()
    @Published var location: CLLocation? = nil

    override init() {
        super.init()
        self.locationManager.delegate = self
        self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
        self.locationManager.distanceFilter = kCLDistanceFilterNone
        self.locationManager.requestWhenInUseAuthorization()
        self.locationManager.startUpdatingLocation()
    }
}

extension LocationManager: CLLocationManagerDelegate {
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        print(status)
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
    
        self.location = location
    }
}//extension

And finally the ContentView:

struct ContentView: View {

    @ObservedObject var locationManager = LocationManager()

    var body: some View {
        MapView()
    }
}

Tapping the map shows a console output like this:

in addAnnotationOnTapGesture

point is (156.0, 515.6666564941406)

coordinate?.latitude is nil

Any guidance would be appreciated. Xcode 12.5 iOS 14.5

JohnSF
  • 3,736
  • 3
  • 36
  • 72

1 Answers1

4

In your Coordinator, you have two references to MapViews. One (control) is set in init and represents the actual view that you want. The other (parent) is defined within your Coordinator and is not actually part of the view hierarchy. Therefore, when you try to get a coordinate from it, it returns nil. You can change all of the references to control and it works:

class Coordinator: NSObject, MKMapViewDelegate {
    var control: MapView

    let sfCoord = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)

    init(_ control: MapView) {
        self.control = control
    }

    func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
        if let annotationView = views.first {
            if let annotation = annotationView.annotation {
                if annotation is MKUserLocation {
                    let region = MKCoordinateRegion(center: annotation.coordinate, latitudinalMeters: 2000, longitudinalMeters: 2000)
                    mapView.setRegion(region, animated: true)
                }
            }
        }
    }//did add

    @objc func addAnnotationOnTapGesture(sender: UITapGestureRecognizer) {
        if sender.state == .ended {
            print("in addAnnotationOnTapGesture")
            let point = sender.location(in: control.myMapView)
            print("point is \(point)")
            let coordinate = control.myMapView?.convert(point, toCoordinateFrom: control.myMapView)
            print("coordinate?.latitude is \(String(describing: coordinate?.latitude))")
            let annotation = MKPointAnnotation()
            annotation.coordinate = coordinate ?? sfCoord
            annotation.title = "Start"
            control.myMapView?.addAnnotation(annotation)
        }
    }
}//coord
jnpdx
  • 45,847
  • 6
  • 64
  • 94