I\'m trying to detect when a finger first makes contact with a view in SwiftUI. I could do this very easily with UIKit Events but can\'t figure this out in SwiftUI.
I
This is a solution to detect changes between states and also the coordinates of the touch (within the Text View
in this case):
I added an enum to manage the states (using began, moved and ended for those UIKit-nostalgic)
import SwiftUI
struct ContentView: View {
@State var touchPoint = CGPoint(x: 0, y: 0)
@State var touchState = TouchState.none
var body: some View {
Text("\(touchState.name): \(Int(self.touchPoint.x)), \(Int(self.touchPoint.y))")
.border(Color.red).font(.largeTitle)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged({ (touch) in
self.touchState = (self.touchState == .none || self.touchState == .ended) ? .began : .moved
self.touchPoint = touch.location
})
.onEnded({ (touch) in
self.touchPoint = touch.location
self.touchState = .ended
})
)
}
}
enum TouchState {
case none, began, moved, ended
var name: String {
return "\(self)"
}
}
Actually, @eli_slade, all you need is this:
LongPressGesture().onChanged {_ in print("down") }
You can use the .updating
modifier like this:
struct TapTestView: View {
@GestureState private var isTapped = false
var body: some View {
let tap = DragGesture(minimumDistance: 0)
.updating($isTapped) { (_, isTapped, _) in
isTapped = true
}
return Text("Tap me!")
.foregroundColor(isTapped ? .red: .black)
.gesture(tap)
}
}
Some notes:
@GestureState
property wrapper automatically resets its value to the original value when the gesture ends. This way you only have to worry about setting isTapped
to true
. It will automatically be false
again when the interaction ends.updating
modifier has this weird closure with three parameters. In this case we are only interested in the middle one. It's an inout
parameter to the wrapped value of the GestureState
, so we can set it here. The first parameter has the current value of the gesture; the third one is a Transaction
containing some animation context.You can create a view modifier this way:
extension View {
func onTouchDownGesture(callback: @escaping () -> Void) -> some View {
modifier(OnTouchDownGestureModifier(callback: callback))
}
}
private struct OnTouchDownGestureModifier: ViewModifier {
@State private var tapped = false
let callback: () -> Void
func body(content: Content) -> some View {
content
.simultaneousGesture(DragGesture(minimumDistance: 0)
.onChanged { _ in
if !self.tapped {
self.tapped = true
self.callback()
}
}
.onEnded { _ in
self.tapped = false
})
}
}
Now you can use it like:
struct MyView: View {
var body: some View {
Text("Hello World")
.onTouchDownGesture {
print("View did tap!")
}
}
}
If you combine the code from these two questions:
How to detect a tap gesture location in SwiftUI?
UITapGestureRecognizer - make it work on touch down, not touch up?
You can make something like this:
ZStack {
Text("Test")
TapView {
print("Tapped")
}
}
struct TapView: UIViewRepresentable {
var tappedCallback: (() -> 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: (() -> Void)
init(tappedCallback: @escaping (() -> Void)) {
self.tappedCallback = tappedCallback
}
@objc func tapped(gesture:UITapGestureRecognizer) {
self.tappedCallback()
}
}
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
}
}
There's definitely some abstractions we can make so that the usage is more like the other SwiftUI Gestures, but this is a start. Hopefully Apple builds in support for this at some point.
DragGesture was a bit unreliable for me, but LongPressGesture works perfectly. Here is a way to call methods when the GestureState value changes:
@GestureState var longPressGestureState = false
var isTouchedDown: Bool {
// use this place to call functions when the value changes
return longPressGestureState
}
var body: View {
Color(isTouchedDown ? .red : .black)
.gesture(
LongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity)
.updating($longPressGestureState) { value, state, _ in
state = value
}
)
}