Michel
Michel

Reputation: 11755

Tapping an MKMapView in SwiftUI

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.

Upvotes: 4

Views: 3226

Answers (3)

Patrik Jurgelj
Patrik Jurgelj

Reputation: 216

I also wanted to add on tap event, but there were lot of problems with preserving default Map gestures. I came up with a function that handles onTapGesture which is available in iOS 16 and newer, for older versions creature a TapGesture and combines with DragGesture which then gives back the location of tap.

import SwiftUI

extension View {

    func onTapGestureButCompatible(
        coordinateSpace: CoordinateSpace = .local,
        perform action: @escaping (CGPoint) -> Void
    ) -> some View {
        if #available(iOS 16.0, *) {
            return onTapGesture(count: 1, coordinateSpace: coordinateSpace, perform: action)
        } else {
        
            let drag = DragGesture(minimumDistance: 0,
                                   coordinateSpace: coordinateSpace)
                                .onEnded { value in
                                    action(value.location)
                                }
        
            return simultaneousGesture(TapGesture().sequenced(before: drag))
        }
    }
}

When creating Map you just add call to the function like so:

Map(coordinateRegion: $region,
    showsUserLocation: true,
    userTrackingMode: $trackingMode,
    annotationItems: locations,
    annotationContent: { location in
       MapPin(coordinate: location.coordinate)
    }
)
.edgesIgnoringSafeArea(.all)
.onTapGestureButCompatible(coordinateSpace: .local) { cgPoint in
    //use cgPoint according to your need, you can convert cgPoint to latitude and longitude using GeometryReader
}

Upvotes: 0

KieranC
KieranC

Reputation: 57

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

Upvotes: 0

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)
        
    }
}
}

Upvotes: 10

Related Questions