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.
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+.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With