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)
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
How to Use Instance Method as Callback For Function Which Takes Only Func or Literal Closure
Wait Until Swift For Loop With Asynchronous Network Requests Finishes Executing
Trying to Understand Asynchronous Operation Subclass
How to Resolve: 'Keywindow' Was Deprecated in iOS 13.0
Alternative to Performselector in Swift
Sort Dictionary by Values in Swift
The Use of Swift 3 @Objc Inference in Swift 4 Mode Is Deprecated
How to Test Equality of Swift Enums With Associated Values
Object X of Class Y Does Not Implement Methodsignatureforselector in Swift
How to Store 1.66 in Nsdecimalnumber
How to Dispatch_Sync, Dispatch_Async, Dispatch_After, etc in Swift 3, Swift 4, and Beyond
Instantiated Optional Variable Shows as Nil in Xcode Debugger
How to Create Usdz File Using Xcode Converter
Creating Custom Tableview Cells in Swift
Why Can't a Get-Only Property Requirement in a Protocol Be Satisfied by a Property Which Conforms