tom hackbarth
tom hackbarth

Reputation: 71

Does anyone have MapKit+LongPress working?

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

Answers (5)

Vicente Garcia
Vicente Garcia

Reputation: 6390

iOS 18

Starting on iOS 18 we have UIGestureRecognizerRepresentable and we can use any UIGestureRecognizer like UILongPressGestureRecognizer in SwiftUI.

Example

The map

MapReader { proxy in
    Map()
        .gesture(MyLongPressGesture { position in
            let coordinate = proxy.convert(position, from: .global)
        })
}

The gesture recognizer

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

Final notes

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

Jordan C.
Jordan C.

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

Brendan White
Brendan White

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

Cherpak Evgeny
Cherpak Evgeny

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

ChrisR
ChrisR

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

Related Questions