Apoptosome
Apoptosome

Reputation: 25

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

Upvotes: 1

Views: 40

Answers (1)

Sweeper
Sweeper

Reputation: 273540

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.

Upvotes: 0

Related Questions