问题
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've tried a DragGesture with minimum movement of 0 but it still won't change until your finger moves.
TapGesture will only work when you lift your finger and LongPressGesture will not trigger fast enough no matter what I set the parameters to.
DragGesture(minimumDistance: 0, coordinateSpace: .local).onChanged({ _ in print("down")})
LongPressGesture(minimumDuration: 0.01, maximumDistance: 100).onEnded({_ in print("down")})
I want to detect a touchDown event as soon as a finger makes contact with a view. Apple's default gestures have restrictions to either distance or time.
回答1:
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.
回答2:
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:
- The zero minimum distance makes sure the gesture is immediately recognised
- The
@GestureState
property wrapper automatically resets its value to the original value when the gesture ends. This way you only have to worry about settingisTapped
totrue
. It will automatically befalse
again when the interaction ends. - The
updating
modifier has this weird closure with three parameters. In this case we are only interested in the middle one. It's aninout
parameter to the wrapped value of theGestureState
, so we can set it here. The first parameter has the current value of the gesture; the third one is aTransaction
containing some animation context.
回答3:
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)"
}
}
回答4:
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!")
}
}
}
回答5:
Actually, @eli_slade, all you need is this:
LongPressGesture().onChanged {_ in print("down") }
😀
Here's a demonstration app:
And here's the code for it:
struct ContentView: View {
@State var isRed = false
var body: some View {
Circle()
.fill(isRed ? Color.red : Color.black)
.frame(width: 150, height: 150)
.gesture(LongPressGesture().onChanged { _ in self.isRed.toggle()})
}
}
来源:https://stackoverflow.com/questions/58784684/how-do-you-detect-a-swiftui-touchdown-event-with-no-movement-or-duration