Tapping an MKMapView in SwiftUI
Asked Answered
T

2

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.

Timberwork answered 27/7, 2020 at 7:33 Comment(0)
A
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)
        
    }
}
}
Attaint answered 27/7, 2020 at 8:15 Comment(7)
I get the error: "Cannot convert value of type 'MapView' to expected argument type 'UIView?'".Timberwork
On the line: "let location = gesture.location(....."Timberwork
typo maybe? let location = gRecognizer.location(in: self.parent.mapView)Attaint
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.Timberwork
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.Timberwork
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.Josie
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"Josie
P
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

Pep answered 14/6, 2023 at 16:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.