📍

SwiftUI MapKit LongPressGesture Detection

2025/03/10に公開

SwiftUI MapKit において、地図を長押ししたらそこにピンを立てるということをしたい。

結論

現状は iOS18 以上で独自の LongPressGesture struct を作って検知するしかない模様。

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))
    }
}
struct MapView: View {
    var body: some View {
        MapReader { proxy in
            Map()
                .gesture(MyLongPressGesture { position in
                    let coordinate = proxy.convert(position, from: .global)
                })
        }
    }
}

https://stackoverflow.com/questions/71399626/does-anyone-have-mapkitlongpress-working/78652172#78652172

updating: 長押しを検知したらピンを立てる

これは結局うまく行かなかった。が .sequenced の使い方などをメモ。
基本的に .sequenced を使うと2つの異なるジェスチャー(ここでは長押しとドラッグ)を組み合わせて検知なり実装することが可能なようで、ここで長押しにドラッグを組み合わせているのは長押しした地図上の位置を検出するために drag が必要だから。

.gesture(
                    LongPressGesture(minimumDuration: 0.5)
                        .sequenced(before: DragGesture(minimumDistance: 0))
                        .updating($isLongPress, body: { value, state, transaction in
                            switch value { // .first が long press gesture 0.5秒
                            case .second(true, let drag): // sequenced で定義しているので .second が DragGesture となる
                                print("drag: ", drag) // FIXME: ここが nil になる。ブレイクポイントで止めたら値は入っているのに
                                if let location = drag?.location {
                                    if let coordinate = proxy.convert(location, from: .local) {
                                        let placemark = MKPlacemark(coordinate: coordinate)
                                        let item = MKMapItem(placemark: placemark)
                                        viewModel.savedMapItems.append(item)
                                    }
                                }
                            default:
                                break
                            }
                        })
                    )

なお .updating のコールバックの引数の使い方として、長押しを検知したらリアルタイムで UI をどうこうしたいときに gestureState = currentState のようにするようだがイマイチピンと来ていない。

struct CounterView: View {
    @GestureState private var isDetectingLongPress = false
    
    var body: some View {
        let press = LongPressGesture(minimumDuration: 1)
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
            }
        
        return Circle()
            .fill(isDetectingLongPress ? Color.yellow : Color.green)
            .frame(width: 100, height: 100, alignment: .center)
            .gesture(press)
    }
}

https://developer.apple.com/documentation/swiftui/adding-interactivity-with-gestures

onEnded: 長押しを離したらピンを立てる

.gesture(
                    LongPressGesture(minimumDuration: 0.5)
                        .sequenced(before: DragGesture(minimumDistance: 0))
                        .onEnded { value in // FIXME: change to onStarted
                            switch value {
                            case .second(true, let drag):
                                if let position = drag?.location {
                                    if let coordinate = proxy.convert(position, from: .local) {
                                        let placemark = MKPlacemark(coordinate: coordinate)
                                        let item = MKMapItem(placemark: placemark)
                                        viewModel.savedMapItems.append(item)
                                    }
                                }
                            default:
                                break
                            }
                        }
                    )

Discussion