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