Reputation: 71
I am trying to get SwiftUI + MapKit + LongPress gesture working. When I add the map within the ContentView
, it works great. I then add the .onLongPressGesture
modifier to the map, and the panning/zooming stops working. Long press works though.
The code I am working on:
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true)
.onLongPressGesture {
// How do I get the location (Lat/Long) I am pressed on?
print("onLongPressGesture")
}
Also, is there a way to get the lat/long from the long press gesture?
Looking to make it work using SwiftUI
only, without wrapping UIKit
within a UIViewRepresentable
.
Upvotes: 5
Views: 1632
Reputation: 6390
Starting on iOS 18 we have UIGestureRecognizerRepresentable and we can use any UIGestureRecognizer
like UILongPressGestureRecognizer in SwiftUI
.
MapReader { proxy in
Map()
.gesture(MyLongPressGesture { position in
let coordinate = proxy.convert(position, from: .global)
})
}
import SwiftUI
struct MyLongPressGesture: UIGestureRecognizerRepresentable {
private let longPressAt: (_ position: CGPoint) -> Void
init(longPressAt: @escaping (_ position: CGPoint) -> Void) {
self.longPressAt = longPressAt
}
func makeUIGestureRecognizer(context: Context) -> UILongPressGestureRecognizer {
UILongPressGestureRecognizer()
}
func handleUIGestureRecognizerAction(_ gesture: UILongPressGestureRecognizer, context: Context) {
guard gesture.state == .began else { return }
longPressAt(gesture.location(in: gesture.view))
}
}
Remember as of June 2024 this is still in Beta and may change quickly.
For more details on this: WWDC 24 What's new in SwiftUI
Upvotes: 3
Reputation: 103
Here's how I was able to get the location of a LongPressGesture, while also preserving the functionality of the Map initializer's selection
parameter (i.e. long pressing not allowed on existing map objects):
struct MapLongPressTestView: View {
@State private var selectedMapItemTag: String? = nil
private let randomCoordinate = CLLocationCoordinate2D(latitude: .random(in: -90...90), longitude: .random(in: -180...180))
var body: some View {
MapReader { proxy in
Map(selection: $selectedMapItemTag) {
// Your Annotations, Markers, Shapes, & other map content
// Selectable Test Example
Annotation(coordinate: randomCoordinate) {
Image(systemName: "water.waves")
.foregroundStyle(.orange)
} label: {
Text("Water, most likely")
}
.tag("example")
}
.gesture(DragGesture())
.gesture(
LongPressGesture(minimumDuration: 1, maximumDistance: 0)
.sequenced(before: SpatialTapGesture(coordinateSpace: .local))
.onEnded { value in
switch value {
case let .second(_, tapValue):
guard let point = tapValue?.location else {
print("Unable to retreive tap location from gesture data.")
return
}
guard let coordinate = proxy.convert(point, from: .local) else {
print("Unable to convert local point to coordinate on map.")
return
}
print("Long press occured at: \(coordinate)")
default: return
}
}
)
}
.onChange(of: selectedMapItemTag) {
print(selectedMapItemTag.map { "\($0) is selected" } ?? "No selection")
}
}
}
Alternatively, here's an extension on the Map
type to reduce boilerplate:
extension Map {
func onLongPressGesture(minimumDuration: Double = 0, maximumDistance: CGFloat = 0, onTouchUp: @escaping (CGPoint) -> Void) -> some View {
self
.gesture(DragGesture())
.gesture(
LongPressGesture(minimumDuration: minimumDuration, maximumDistance: maximumDistance)
.sequenced(before: SpatialTapGesture(coordinateSpace: .local))
.onEnded { value in
switch value {
case .second(_, let tapValue):
guard let point = tapValue?.location else {
print("Unable to retreive tap location from gesture data.")
return
}
onTouchUp(point)
default: return
}
}
)
}
}
It would be used like so:
MapReader { proxy in
Map(selection: $selectedMapItemTag) {
// ...
}
.onLongPressGesture(minimumDuration: 1) { point in
if let coordinate = proxy.convert(point, from: .local) {
print("Long press occured at: \(coordinate)")
}
}
}
SpacialTapGesture
is available from iOS 16+.
Upvotes: 6
Reputation: 453
To answer the last part of your question - Paul Hudson shows how to get the tap location in lat/long on this page [EDIT - doesn't seem to work for long press]
MapReader { proxy in
Map()
.onTapGesture { position in
if let coordinate = proxy.convert(position, from: .local) {
print(coordinate)
}
}
}
Upvotes: 0
Reputation: 2770
Yep.
I wrapped MKMapView
in UIViewRepresentable
and added long press gesture recognizer in the delegate method like this:
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// We need to update the region when the user changes it
// otherwise when we zoom the mapview will return to its original region
DispatchQueue.main.async {
if self.longPress == nil {
let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(recognizer:)))
mapView.addGestureRecognizer(recognizer)
self.longPress = recognizer
}
self.parent.region = mapView.region
}
}
Then
@objc func longPressGesture(recognizer: UILongPressGestureRecognizer) {
if let mapView = recognizer.view as? MKMapView {
let touchPoint = recognizer.location(in: mapView)
let touchMapCoordinate = mapView.convert(touchPoint, toCoordinateFrom: mapView)
// TODO: check if we already have this location and bail out
if annotations.count > 0 {
return
}
let annotation = Annotation(title: touchMapCoordinate.stringValue, coordinate: touchMapCoordinate)
mapView.removeAnnotations(annotations.compactMap({ $0.pointAnnotation }))
annotations.append(annotation)
mapView.addAnnotation(annotation.pointAnnotation)
}
}
Using the code from this answer: https://stackoverflow.com/a/71698741/1320010
Upvotes: 0
Reputation: 12165
Don't ask why but this seems to work:
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true)
.gesture(DragGesture())
.onLongPressGesture {
print("Here!")
}
Upvotes: 4