Reputation: 11755
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
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
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
}
}
Upvotes: 0
Reputation: 36782
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