Capture touchDown location of onLongPressGesture in swiftUI?
Asked Answered
M

2

11

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.

Mischance answered 10/7, 2020 at 15:40 Comment(0)
R
10

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)
        )
    }
}
Rapacious answered 11/7, 2020 at 12:15 Comment(3)
For some reason it is only working on canvas preview. In the simulator when I long press, the location is .zero so the Rectangle() is shown in the .zero CGPointDelvecchio
This doesn't work - drag is nil until the user moves the finger! If she lifts it without moving, there is nothing to read the location from. I really really dislike how SwiftUI left out useful info from the gesture events.Aileen
This doesn't work until you release the long press.Freeway
P
0

There is another workaround based on Gesture.simultaneously.

You can immediately get the CGPoint after the long press be detected, rather than get the point until you release the long press.

Note that the .onTapGesture is neccesary.

struct ContentView: View {
    
    @GestureState var state: (Bool, CGPoint) = (false, .zero)
    
    var body: some View {
        Rectangle().foregroundStyle(.cyan)
            .onTapGesture { }
            .gesture(
                LongPressGesture().simultaneously(with: DragGesture(minimumDistance: 0, coordinateSpace: .local))
                    .updating($state, body: { value, state, trans in
                        state.0 = value.first ?? false
                        state.1 = value.second?.location ?? .zero
                        print(state)
                    })
                    .onChanged({ v in
                        
                    })
                    .onEnded({ v in
                        
                    })
            )
    }
}

It works for me, but i think there might be a better way to do this.

Psych answered 26/8 at 10:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.