(For SwiftUI, not vanilla UIKit) Very simple example code to, say, display red boxes on a gray background:
struct ContentView : View {
@State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
var body: some View {
return ZStack {
Color.gray
.tapAction {
// TODO: add an entry to self.points of the location of the tap
}
ForEach(self.points.identified(by: \.debugDescription)) {
point in
Color.red
.frame(width:50, height:50, alignment: .center)
.offset(CGSize(width: point.x, height: point.y))
}
}
}
}
I'm assuming instead of tapAction, I need to have a TapGesture or something? But even there I don't see any way to get information on the location of the tap. How would I go about this?
Starting form iOS 17 / macOS 14, the onTapGesture
modifier makes available the location of the tap/click in the action closure:
struct ContentView: View {
var body: some View {
Rectangle()
.frame(width: 200, height: 200)
.onTapGesture { location in
print("Tapped at \(location)")
}
}
}
The most correct and SwiftUI-compatible implementation I come up with is this one. You can use it like any regular SwiftUI gesture and even combine it with other gestures, manage gesture priority, etc...
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> {
self.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)
}
}
}
The above code defines a struct conforming to SwiftUI Gesture
protocol. This gesture is a combinaison of a TapGesture
and a DragGesture
. This is required to ensure that the gesture was a tap and to retrieve the tap location at the same time.
The onEnded
method checks that both gestures occurred and returns the location as a CGPoint through the escaping closure passed as parameter. The two last guard
statements are here to handle multiple tap gestures, as the user can tap slightly different locations, those lines introduce a tolerance of 1 point, this can be changed if ones want more flexibility.
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)
}
}
Finally View
extensions are defined to offer the same API as onDragGesture
and other native gestures.
Use it like any SwiftUI gesture:
struct ContentView : View {
@State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
var body: some View {
return ZStack {
Color.gray
.onClickGesture { point in
points.append(point)
}
ForEach(self.points.identified(by: \.debugDescription)) {
point in
Color.red
.frame(width:50, height:50, alignment: .center)
.offset(CGSize(width: point.x, height: point.y))
}
}
}
}