iosswiftswiftuixcode16

Tap Map to Mark Point


I want to make it so someone can tap on a Map and do the following:

Many other apps like Uber or DoorDash have this be possible, but I can't find anything relevant on it online. Articles recommend this method, but if I try making a Marker, it says this was deprecated. If I try getting tapLocation the location is not in world coordinates so its not useful.

Map()
  .onTapGesture { tapLocation in
    print(tapLocation)
  }
  .mapControls {
    MapUserLocationButton()
  }

Currently I have some nasty code to use a MKMapView with a UITapGestureRecognizer attached to it. The user can tap a point and it gets the tap location converted to the coordinate location from the MKMapView and set as a MKPointAnnotation(). The Users location is also set as a state variable so this forces redraws of the USER Location pin constantly (nasty).

import SwiftUI
import MapKit

struct MapInput: UIViewRepresentable {
    @Binding var tappedCoordinate: CLLocationCoordinate2D?
    @Binding var userLocation: CLLocation?

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        
        // Add Tap Gesture Recognizer
        let tapGestureRecognizer = UITapGestureRecognizer(
            target: context.coordinator,
            action: #selector(Coordinator.handleTap(gesture:))
        )
        mapView.addGestureRecognizer(tapGestureRecognizer)
        
        return mapView
    }

    func updateUIView(_ uiView: MKMapView, context: Context) {
        // Remove existing annotations
        uiView.removeAnnotations(uiView.annotations)
        
        // Add new annotation if there's a tapped coordinate
        if let coordinate = tappedCoordinate {
            let annotation = MKPointAnnotation()
            annotation.coordinate = coordinate
            annotation.title = "POI"
            uiView.addAnnotation(annotation)
        }
        
        if let user = userLocation {
            let annotation = MKPointAnnotation()
            annotation.coordinate = user.coordinate
            annotation.title = "USER"
            uiView.addAnnotation(annotation)
        }
    }

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

    class Coordinator: NSObject {
        var parent: MapInput

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

        @objc func handleTap(gesture: UITapGestureRecognizer) {
            let mapView = gesture.view as! MKMapView
            let touchPoint = gesture.location(in: mapView)
            let coordinate = mapView.convert(touchPoint, toCoordinateFrom: mapView)
            parent.tappedCoordinate = coordinate
        }
    }
}

Solution

  • You can convert a CGPoint to/from a CLLocationCoordinate2D using a MapReader, using the convert(_:from:) method.

    But first, you need a structure to store information about the markers on the map, e.g.

    struct MarkerInfo: Hashable, Identifiable {
        let lat: CLLocationDegrees
        let lon: CLLocationDegrees
        let id = UUID()
    }
    

    Then, store an array of these in a @State, and append to it in onTapGesture:

    struct ContentView: View {
        @State private var markers = [MarkerInfo]()
        
        var body: some View {
            MapReader { mapProxy in
                Map {
                    UserAnnotation() // shows the user's location
                    ForEach(markers) { marker in
                        Marker("Some Title", coordinate: .init(latitude: marker.lat, longitude: marker.lon))
                    }
                }
                .mapControls {
                    MapUserLocationButton()
                }
                .onTapGesture { tapLocation in
                    guard let coordinate = mapProxy.convert(tapLocation, from: .local) else {
                        print("there is no map!")
                        return
                    }
                    markers.append(.init(lat: coordinate.latitude, lon: coordinate.longitude))
                }
            }
        }
    }
    

    Note that the coordinate space that onTapGesture is .local by default, hence we use .local when converting the CGPoint too.