4

I have a map in a SwiftUI app. It is working up to a point; but now I want to be able to tap on it and know the latitude and longitude of the tap. Here is the current code:

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    @Binding var centerCoordinate: CLLocationCoordinate2D
    
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        
        let gRecognizer = UITapGestureRecognizer(target: context.coordinator,
                                action: #selector(Coordinator.tapHandler(_:)))
        mapView.addGestureRecognizer(gRecognizer)
        return mapView
    }

    func updateUIView(_ view: MKMapView, context: Context) {
        //print(#function)
    }

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

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView

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

        let gRecognizer = UITapGestureRecognizer(target: self,
                                                 action: #selector(tapHandler(_:)))
        
        @objc func tapHandler(_ gesture: UITapGestureRecognizer) {
            print(#function)
            .... get useful information here ...
        }
    }
}

In this state I can see when I tap, but I don't get the information I need (.i.e coordinates of the tap). I have tried a few variations of the code after searching the net. At this point it is not yet working. Any relevant tip on the way to go would be very welcome.

Michel
  • 10,303
  • 17
  • 82
  • 179

2 Answers2

10

I had a similar situation, and this is what I did. I made Coordinator UIGestureRecognizerDelegate, and ensure gRecognizer delegate is set to it, and add it to the map. Something like:

struct MapView: UIViewRepresentable {
@Binding var centerCoordinate: CLLocationCoordinate2D

let mapView = MKMapView()

func makeUIView(context: Context) -> MKMapView {
    mapView.delegate = context.coordinator
    return mapView
}

func updateUIView(_ view: MKMapView, context: Context) {
    //print(#function)
}

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

class Coordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate {
    var parent: MapView

    var gRecognizer = UITapGestureRecognizer()

    init(_ parent: MapView) {
        self.parent = parent
        super.init()
        self.gRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapHandler)) 
        self.gRecognizer.delegate = self
        self.parent.mapView.addGestureRecognizer(gRecognizer)
    }

    @objc func tapHandler(_ gesture: UITapGestureRecognizer) {
        // position on the screen, CGPoint
        let location = gRecognizer.location(in: self.parent.mapView)
        // position on the map, CLLocationCoordinate2D
        let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView)
        
    }
}
}
  • I get the error: "Cannot convert value of type 'MapView' to expected argument type 'UIView?'". – Michel Jul 27 '20 at 08:34
  • On the line: "let location = gesture.location(....." – Michel Jul 27 '20 at 08:51
  • typo maybe? let location = gRecognizer.location(in: self.parent.mapView) – workingdog support Ukraine Jul 27 '20 at 08:53
  • 1
    It is a difference between your code and mine. You have a mapView member and I don't. I need to change to try out exactly your suggestion. – Michel Jul 27 '20 at 09:01
  • that was a matter of tweaking my code a bit more to make it on the same wave length as yours. It finally works, thanks for your tip. – Michel Jul 27 '20 at 09:41
  • THANKS - SUPER HELPFUL. I never would have figured this out on my own. One change I made is Instead of passing the 'struct' with the parent into the init, I just passed the parent's member MKMapView into the init since that's all you reference. Since the MKMapView is a class and the parent is a struct it avoids possible future confusion when you think your using the parent but really using a copy of the parent. – bhause Nov 09 '21 at 17:30
  • You must use @State when declaring the 'parent: MapView' member variable or else it will get replaced with a bogus copy when ever the View is re-created and you'll be referencing the wrong copy of parent. E.g. Change this: "var parent: MapView" to this "@State var parent: MapView" – bhause Nov 12 '21 at 00:35
0

I had a similar problem when trying to get the clicked location on a map for my SwiftUI app running on macOS, so obviously, I have to use NSViewRepresentable instead of UIViewRepresentable.

The answer by @workingdog helped so much; here is my version for macOS that displays the clicked location in a Text view, Z-Stacked on top of the map:

struct MapViewRepresentable: NSViewRepresentable {
  @Binding var clickedCoordinate: CLLocationCoordinate2D
  var initialLocation: CLLocationCoordinate2D
  var initialSpan: MKCoordinateSpan

  let mapView = MKMapView()
  
  func makeNSView(context: Context) -> MKMapView {
    mapView.preferredConfiguration = MKHybridMapConfiguration(elevationStyle: .realistic)
    mapView.region = MKCoordinateRegion(center: initialLocation, span: initialSpan)
    mapView.delegate = context.coordinator
    return mapView
  }
  
  func updateNSView(_ nsView: MKMapView, context: Context) {
    
  }
  
  func makeCoordinator() -> Coordinator {
    return Coordinator(self)
  }
  
  class Coordinator: NSObject, MKMapViewDelegate, NSGestureRecognizerDelegate {
    @State var parent: MapViewRepresentable
    
    var gRecognizer = NSClickGestureRecognizer()
    
    init(_ parent: MapViewRepresentable) {
      self.parent = parent
      super.init()
      self.gRecognizer = NSClickGestureRecognizer(target: self, action: #selector(tapHandler))
      self.gRecognizer.delegate = self
      self.parent.mapView.addGestureRecognizer(gRecognizer)
    }
    
    @objc func tapHandler(_ gesture: NSClickGestureRecognizer) {
      let location = gesture.location(in: self.parent.mapView)
      let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView)
      parent.clickedCoordinate = coordinate
    }
  }
}

struct MapView: View {
  private static let initialLocation = CLLocationCoordinate2D(latitude: 51.5, longitude: 0.0)  // London, UK
  private static let initialSpan = MKCoordinateSpan(latitudeDelta: 1.0, longitudeDelta: 1.0)

  @State private var clickedCoordinate: CLLocationCoordinate2D = Self.initialLocation

  var body: some View {
    ZStack {
      MapViewRepresentable(clickedCoordinate: $clickedCoordinate, initialLocation: Self.initialLocation, initialSpan: Self.initialSpan)
      HStack {
        Spacer()
        VStack(alignment: .trailing) {
          Spacer()
          Text("Clicked - Lat: \(latAsStr(clickedCoordinate.latitude)) Lon: \(lonAsStr(clickedCoordinate.longitude))")
            .background(.white).opacity(0.75)
            .foregroundColor(.black)
            .textSelection(.disabled)
            .padding(5)
        }
      }
    }
  }
  
  func latAsStr(_ lat: Double) -> String { 
    // convert lat to string
  }
  
  func lonAsStr(_ lon: Double) -> String {
    // convert lon to string
  }

}

enter image description here

KieranC
  • 57
  • 6