如何检测没有移动或持续时间的 SwiftUI touchDown 事件?
How do you detect a SwiftUI touchDown event with no movement or duration?
我正在尝试检测手指何时首次接触 SwiftUI 中的视图。我可以使用 UIKit 事件轻松地做到这一点,但无法在 SwiftUI 中解决这个问题。
我尝试了最小移动量为 0 的 DragGesture,但在您的手指移动之前它仍然不会改变。
TapGesture 只会在你抬起手指时起作用,而 LongPressGesture 无论我设置什么参数都不会足够快地触发。
DragGesture(minimumDistance: 0, coordinateSpace: .local).onChanged({ _ in print("down")})
LongPressGesture(minimumDuration: 0.01, maximumDistance: 100).onEnded({_ in print("down")})
我想在手指与视图接触时立即检测 touchDown 事件。 Apple 的默认手势对距离或时间有限制。
更新:这不再是问题,因为 Apple 似乎已经更新了 DragGesture 的工作方式,或者我遇到了特定的上下文错误。
如果结合这两个问题的代码:
How to detect a tap gesture location in SwiftUI?
UITapGestureRecognizer - make it work on touch down, not touch up?
你可以做这样的事情:
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
}
}
我们肯定可以进行一些抽象,以便使用起来更像其他 SwiftUI 手势,但这是一个开始。希望 Apple 在某个时候建立对此的支持。
您可以像这样使用 .updating
修饰符:
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)
}
}
一些注意事项:
- 零最小距离确保立即识别手势
@GestureState
属性 包装器会在手势结束时自动将其值重置为原始值。这样你只需要担心将 isTapped
设置为 true
。互动结束后会自动再次false
updating
修饰符有一个带有三个参数的奇怪闭包。在这种情况下,我们只对中间的感兴趣。它是 GestureState
包装值的 inout
参数,所以我们可以在这里设置它。第一个参数是手势的当前值;第三个是 Transaction
包含一些动画上下文。
这是检测状态之间变化以及触摸坐标(在本例中为 Text View
内)的解决方案:
我添加了一个枚举来管理状态(使用那些 UIKit-怀旧的开始、移动和结束)
导入 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)"
}
}
您可以这样创建视图修改器:
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
})
}
}
现在您可以像这样使用它:
struct MyView: View {
var body: some View {
Text("Hello World")
.onTouchDownGesture {
print("View did tap!")
}
}
}
其实,@eli_slade,你只需要这个:
LongPressGesture().onChanged {_ in print("down") }
这是一个演示应用程序:
这是它的代码:
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()})
}
}
这是一个 watchOS-specific 答案,因为 isTouchedDown
定义中的内容在 [=25] 上以一种奇怪的方式被多次调用=]. iOS 如果您想在触摸视图 up/down 时执行 运行 操作,例如 .
,则有更好的解决方案
注意:当容器屏幕出现时,isTouchedDown
定义仍会触发几次。一个 hacky 解决方案是布尔值 shouldCallIsTouchedDownCode
在 onAppear
.
0.3 秒后变为 false
@GestureState var longPressGestureState = false
@State var shouldCallIsTouchedDownCode = false
var isTouchedDown: Bool {
guard shouldCallIsTouchedDownCode else {
return
}
// 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
}
)
.onAppear {
shouldCallIsTouchedDownCode = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
shouldCallIsTouchedDownCode = true
}
}
}
对于iOS,这里有一个检测所有手势状态的解决方案。使用来自 .
的代码
import SwiftUI
import UIKit
struct TouchDownView: UIViewRepresentable {
typealias TouchDownCallback = ((_ state: UIGestureRecognizer.State) -> Void)
var callback: TouchDownCallback
func makeUIView(context: UIViewRepresentableContext<TouchDownView>) -> TouchDownView.UIViewType {
let view = UIView(frame: .zero)
let gesture = UILongPressGestureRecognizer(
target: context.coordinator,
action: #selector(Coordinator.gestureRecognized)
)
gesture.minimumPressDuration = 0
view.addGestureRecognizer(gesture)
return view
}
class Coordinator: NSObject {
var callback: TouchDownCallback
init(callback: @escaping TouchDownCallback) {
self.callback = callback
}
@objc fileprivate func gestureRecognized(gesture: UILongPressGestureRecognizer) {
callback(gesture.state)
}
}
func makeCoordinator() -> TouchDownView.Coordinator {
return Coordinator(callback: callback)
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<TouchDownView>) {
}
}
用法
TouchDownView { state in
switch state {
case .began: print("gesture began")
case .ended: print("gesture ended")
case .cancelled, .failed: print("gesture cancelled/failed")
default: break
}
}
我已经回答了这个 ,但也值得在这个更受欢迎的问题上发帖。
您可以在 View
上使用隐藏的 _onButtonGesture
方法,即 public。它甚至不需要附加到 Button
,但它看起来更好,因为你看到了按下效果。
代码:
struct ContentView: View {
@State private var counter = 0
@State private var pressing = false
var body: some View {
VStack(spacing: 30) {
Text("Count: \(counter)")
Button("Increment") {
counter += 1
}
._onButtonGesture { pressing in
self.pressing = pressing
} perform: {}
Text("Pressing button: \(pressing ? "yes" : "no")")
}
}
}
结果:
由于帧速率的原因,作为 GIF 看起来不太好,但是每当您按下 pressing
时会转到 true
,然后 false
释放.
我正在尝试检测手指何时首次接触 SwiftUI 中的视图。我可以使用 UIKit 事件轻松地做到这一点,但无法在 SwiftUI 中解决这个问题。
我尝试了最小移动量为 0 的 DragGesture,但在您的手指移动之前它仍然不会改变。
TapGesture 只会在你抬起手指时起作用,而 LongPressGesture 无论我设置什么参数都不会足够快地触发。
DragGesture(minimumDistance: 0, coordinateSpace: .local).onChanged({ _ in print("down")})
LongPressGesture(minimumDuration: 0.01, maximumDistance: 100).onEnded({_ in print("down")})
我想在手指与视图接触时立即检测 touchDown 事件。 Apple 的默认手势对距离或时间有限制。
更新:这不再是问题,因为 Apple 似乎已经更新了 DragGesture 的工作方式,或者我遇到了特定的上下文错误。
如果结合这两个问题的代码:
How to detect a tap gesture location in SwiftUI?
UITapGestureRecognizer - make it work on touch down, not touch up?
你可以做这样的事情:
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
}
}
我们肯定可以进行一些抽象,以便使用起来更像其他 SwiftUI 手势,但这是一个开始。希望 Apple 在某个时候建立对此的支持。
您可以像这样使用 .updating
修饰符:
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)
}
}
一些注意事项:
- 零最小距离确保立即识别手势
@GestureState
属性 包装器会在手势结束时自动将其值重置为原始值。这样你只需要担心将isTapped
设置为true
。互动结束后会自动再次false
updating
修饰符有一个带有三个参数的奇怪闭包。在这种情况下,我们只对中间的感兴趣。它是GestureState
包装值的inout
参数,所以我们可以在这里设置它。第一个参数是手势的当前值;第三个是Transaction
包含一些动画上下文。
这是检测状态之间变化以及触摸坐标(在本例中为 Text View
内)的解决方案:
我添加了一个枚举来管理状态(使用那些 UIKit-怀旧的开始、移动和结束)
导入 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)"
}
}
您可以这样创建视图修改器:
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
})
}
}
现在您可以像这样使用它:
struct MyView: View {
var body: some View {
Text("Hello World")
.onTouchDownGesture {
print("View did tap!")
}
}
}
其实,@eli_slade,你只需要这个:
LongPressGesture().onChanged {_ in print("down") }
这是一个演示应用程序:
这是它的代码:
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()})
}
}
这是一个 watchOS-specific 答案,因为 isTouchedDown
定义中的内容在 [=25] 上以一种奇怪的方式被多次调用=]. iOS 如果您想在触摸视图 up/down 时执行 运行 操作,例如
注意:当容器屏幕出现时,isTouchedDown
定义仍会触发几次。一个 hacky 解决方案是布尔值 shouldCallIsTouchedDownCode
在 onAppear
.
@GestureState var longPressGestureState = false
@State var shouldCallIsTouchedDownCode = false
var isTouchedDown: Bool {
guard shouldCallIsTouchedDownCode else {
return
}
// 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
}
)
.onAppear {
shouldCallIsTouchedDownCode = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
shouldCallIsTouchedDownCode = true
}
}
}
对于iOS,这里有一个检测所有手势状态的解决方案。使用来自
import SwiftUI
import UIKit
struct TouchDownView: UIViewRepresentable {
typealias TouchDownCallback = ((_ state: UIGestureRecognizer.State) -> Void)
var callback: TouchDownCallback
func makeUIView(context: UIViewRepresentableContext<TouchDownView>) -> TouchDownView.UIViewType {
let view = UIView(frame: .zero)
let gesture = UILongPressGestureRecognizer(
target: context.coordinator,
action: #selector(Coordinator.gestureRecognized)
)
gesture.minimumPressDuration = 0
view.addGestureRecognizer(gesture)
return view
}
class Coordinator: NSObject {
var callback: TouchDownCallback
init(callback: @escaping TouchDownCallback) {
self.callback = callback
}
@objc fileprivate func gestureRecognized(gesture: UILongPressGestureRecognizer) {
callback(gesture.state)
}
}
func makeCoordinator() -> TouchDownView.Coordinator {
return Coordinator(callback: callback)
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<TouchDownView>) {
}
}
用法
TouchDownView { state in
switch state {
case .began: print("gesture began")
case .ended: print("gesture ended")
case .cancelled, .failed: print("gesture cancelled/failed")
default: break
}
}
我已经回答了这个
您可以在 View
上使用隐藏的 _onButtonGesture
方法,即 public。它甚至不需要附加到 Button
,但它看起来更好,因为你看到了按下效果。
代码:
struct ContentView: View {
@State private var counter = 0
@State private var pressing = false
var body: some View {
VStack(spacing: 30) {
Text("Count: \(counter)")
Button("Increment") {
counter += 1
}
._onButtonGesture { pressing in
self.pressing = pressing
} perform: {}
Text("Pressing button: \(pressing ? "yes" : "no")")
}
}
}
结果:
由于帧速率的原因,作为 GIF 看起来不太好,但是每当您按下 pressing
时会转到 true
,然后 false
释放.