如何检测没有移动或持续时间的 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 解决方案是布尔值 shouldCallIsTouchedDownCodeonAppear.

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 释放.