How to Detect a Tap Gesture Location in Swiftui

How to detect tap location in SwiftUI?

Add DragGesture with minimumDistance: 0 to your body, example:

var body: some View {
VStack
{

}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.background(Color.red)
.gesture(DragGesture(minimumDistance: 0).onEnded({ (value) in
print(value.location)
}))
}

How to detect a tap on a link and tap coordinates at the same time in SwiftUI?

You need to use simultaneousGesture(_:including:). This is because you are clicking a link already so a normal gesture does not occur. Using simultaneousGesture means you both click the link and can grab the coordinates, at the same time.

Code:

Text(makeAttributedString()).padding()
.simultaneousGesture(
/* ... */
)

iOS SwiftUI: How to detect location of UITapGesture on view?

OK, the first problem is your selector definition in the tapGesture declaration. Change it to this:

let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tappedMap(sender:)))

Also, you need to manage the priority of gesture recognizers, since MGLMapView includes UIGestureRecognizer logic internally. I found a discussion of here:

Adding your own gesture recognizer to MGLMapView will block the
corresponding gesture recognizer built into MGLMapView. To avoid
conflicts, define which gesture takes precedence.

You can try this code in your project (pretty much just copied from the linked page above) EDIT: I needed to change the order of lines from my original answer:

let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: Selector("tappedMap"))

// HERE'S THE NEW STUFF:
for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer {
gestureRecognizer.require(toFail: recognizer)
}

mapView.addGestureRecognizer(gestureRecognizer)

EDIT: I was able to test this.

I'm a little confused by the logic inside your tappedMap selector: maybe you meant something like this?

@objc func tappedMap(sender: UITapGestureRecognizer) {
let locationInMap = sender.location(in: control.mapView)
let coordinateSet = sender.view?.convert(locationInMap, to: control.mapView)
}

Get mouse/pointer position for tap gestures in SwiftUI for macOS apps

Update iOS 16

Starting form iOS 16 / macOS 13, the new onTapGesture modifier makes available the location of the tap/click in the action closure as a CGPoint:

struct ContentView: View {
var body: some View {
Rectangle()
.frame(width: 200, height: 200)
.onTapGesture { location in
print("Tapped at \(location)")
}
}
}


Original Answer

Edit

I found a better solution by experimenting with simultaneous gestures. This solution integrated perfectly with SwiftUI gestures and works similarly to other existing gestures:

import SwiftUI

struct ClickGesture: Gesture {
let count: Int
let coordinateSpace: CoordinateSpace

typealias Value = SimultaneousGesture<TapGesture, DragGesture>.Value

init(count: Int = 1, coordinateSpace: CoordinateSpace = .local) {
precondition(count > 0, "Count must be greater than or equal to 1.")
self.count = count
self.coordinateSpace = coordinateSpace
}

var body: SimultaneousGesture<TapGesture, DragGesture> {
SimultaneousGesture(
TapGesture(count: count),
DragGesture(minimumDistance: 0, coordinateSpace: coordinateSpace)
)
}

func onEnded(perform action: @escaping (CGPoint) -> Void) -> _EndedGesture<ClickGesture> {
ClickGesture(count: count, coordinateSpace: coordinateSpace)
.onEnded { (value: Value) -> Void in
guard value.first != nil else { return }
guard let location = value.second?.startLocation else { return }
guard let endLocation = value.second?.location else { return }
guard ((location.x-1)...(location.x+1)).contains(endLocation.x),
((location.y-1)...(location.y+1)).contains(endLocation.y) else {
return
}

action(location)
}
}
}

extension View {
func onClickGesture(
count: Int,
coordinateSpace: CoordinateSpace = .local,
perform action: @escaping (CGPoint) -> Void
) -> some View {
gesture(ClickGesture(count: count, coordinateSpace: coordinateSpace)
.onEnded(perform: action)
)
}

func onClickGesture(
count: Int,
perform action: @escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: count, coordinateSpace: .local, perform: action)
}

func onClickGesture(
perform action: @escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: 1, coordinateSpace: .local, perform: action)
}
}


Previous Workaround

Based on this SO question linked in a comment, I adapted the UIKit solution suitable for macOS.

Apart from changing the types I added a computation to compute the position in the macOS coordinate system style. There is still an ugly force unwrap but I do not know enough AppKit to remove it safely.

You should use this modifier before other gestures modifiers if you want multiple simultaneous gestures on one view.

Currently the CoordinateSpace argument only works for .local and .global but is not implemented correctly for .named(Hashable).

If you have solutions for some of those issues I'll update the answer.

Here is the code:

public extension View {
func onTapWithLocation(count: Int = 1, coordinateSpace: CoordinateSpace = .local, _ tapHandler: @escaping (CGPoint) -> Void) -> some View {
modifier(TapLocationViewModifier(tapHandler: tapHandler, coordinateSpace: coordinateSpace, count: count))
}
}

fileprivate struct TapLocationViewModifier: ViewModifier {
let tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
let count: Int

func body(content: Content) -> some View {
content.overlay(
TapLocationBackground(tapHandler: tapHandler, coordinateSpace: coordinateSpace, numberOfClicks: count)
)
}
}

fileprivate struct TapLocationBackground: NSViewRepresentable {
let tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
let numberOfClicks: Int

func makeNSView(context: NSViewRepresentableContext<TapLocationBackground>) -> NSView {
let v = NSView(frame: .zero)
let gesture = NSClickGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapped))
gesture.numberOfClicksRequired = numberOfClicks
v.addGestureRecognizer(gesture)
return v
}

final class Coordinator: NSObject {
let tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace

init(handler: @escaping ((CGPoint) -> Void), coordinateSpace: CoordinateSpace) {
self.tapHandler = handler
self.coordinateSpace = coordinateSpace
}

@objc func tapped(gesture: NSClickGestureRecognizer) {
let height = gesture.view!.bounds.height
var point = coordinateSpace == .local
? gesture.location(in: gesture.view)
: gesture.location(in: nil)
point.y = height - point.y
tapHandler(point)
}
}

func makeCoordinator() -> TapLocationBackground.Coordinator {
Coordinator(handler: tapHandler, coordinateSpace: coordinateSpace)
}

func updateNSView(_: NSView, context _: NSViewRepresentableContext<TapLocationBackground>) { }
}

Capture touchDown location of onLongPressGesture in 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)

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

backup



Related Topics



Leave a reply



Submit