iosswiftswiftui

Capture touchDown location of onLongPressGesture in swiftUI?


I'm trying to implement a custom context menu that will appear after a long press at the location the user touched. I have been unable to find a way to capture the XY location of a touch down event for onLongPressGesture.

This is where I started

struct ExampleView: View {
    @State var showCustomContextMenu = false
    @State var longPressLocation = CGPoint.zero
    
    var body: some View {
        Rectangle()
            .foregroundColor(Color.green)
            .frame(width: 100.0, height: 100.0)
            .onLongPressGesture {
                print("OnLongPressGesture")
                self.showCustomContextMenu = true
            }
            .overlay(
                Rectangle()
                    .foregroundColor(Color.red)
                    .frame(width: 50.0, height: 50.0)
                    .position(longPressLocation) // <----- this is what I need to capture.
                    .opacity( (showCustomContextMenu) ? 1 : 0 )
        )
    }
}

After looking at this question (and the other SO questions linked in the answer) I tried the following.

How do you detect a SwiftUI touchDown event with no movement or duration?

struct ExampleView: View {
    @State var showCustomContextMenu = false
    @State var longPressLocation = CGPoint.zero
    
    var body: some View {
        ZStack{
            Rectangle()
                .foregroundColor(Color.green)
                .frame(width: 100.0, height: 100.0)
                .onLongPressGesture {
                    print("OnLongPressGesture")
                    self.showCustomContextMenu = true
                }
                .overlay(
                    Rectangle()
                        .foregroundColor(Color.red)
                        .frame(width: 50.0, height: 50.0)
                        .position(longPressLocation)
                        .opacity( (showCustomContextMenu) ? 1 : 0 )
            )
            TapView { point in
                self.longPressLocation = point
                print("Point: \(point)")
            }.background(Color.gray).opacity(0.5)
        }
    }
}

struct TapView: UIViewRepresentable {
    var tappedCallback: ((CGPoint) -> Void)

    func makeUIView(context: UIViewRepresentableContext<TapView>) -> TapView.UIViewType {
        let v = UIView(frame: .zero)
        let gesture = SingleTouchDownGestureRecognizer(target: context.coordinator,
                                                       action: #selector(Coordinator.tapped))
        v.addGestureRecognizer(gesture)
        return v
    }

    class Coordinator: NSObject {
        var tappedCallback: ((CGPoint) -> Void)

        init(tappedCallback: @escaping ((CGPoint) -> Void)) {
            self.tappedCallback = tappedCallback
        }

        @objc func tapped(gesture:UITapGestureRecognizer) {
            self.tappedCallback(gesture.location(in: gesture.view))
        }
    }

    func makeCoordinator() -> TapView.Coordinator {
        return Coordinator(tappedCallback:self.tappedCallback)
    }

    func updateUIView(_ uiView: UIView,
                      context: UIViewRepresentableContext<TapView>) {
    }
}

class SingleTouchDownGestureRecognizer: UIGestureRecognizer {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        if self.state == .possible {
            self.state = .recognized
        }
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .failed
    }
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .failed
    }
}

This almost works, however, the problem this leaves me with is that since Rectangle() and TapView() are in a ZStack depending on where I place them in code I get either the touchDown location or the onLongPressGesture but not both.

Other SO questions I've looked at but ran into similar problems are linked below

How to detect a tap gesture location in SwiftUI?

Swift: Long Press Gesture Recognizer - Detect taps and Long Press This one might be what I'm looking for but I'm not sure how to adapt it to SwiftUI.


Solution

  • Here is a demo of possible approach. It needs a combination of two gestures: LongPress to detect long press and Drag to detect location.

    Tested with Xcode 12 / iOS 14. (on below systems it might be needed to add self. to some properties usage)

    demo

    struct ExampleView: View {
        @State var showCustomContextMenu = false
        @State var longPressLocation = CGPoint.zero
    
        var body: some View {
            Rectangle()
                .foregroundColor(Color.green)
                .frame(width: 100.0, height: 100.0)
                .onTapGesture { showCustomContextMenu = false } // just for demo
                .gesture(LongPressGesture(minimumDuration: 1).sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .local))
                    .onEnded { value in
                        switch value {
                            case .second(true, let drag):
                                longPressLocation = drag?.location ?? .zero   // capture location !!
                                showCustomContextMenu = true
                            default:
                                break
                        }
                })
                .overlay(
                    Rectangle()
                        .foregroundColor(Color.red)
                        .frame(width: 50.0, height: 50.0)
                        .position(longPressLocation)
                        .opacity( (showCustomContextMenu) ? 1 : 0 )
                        .allowsHitTesting(false)
            )
        }
    }