如何在没有移动或持续时间的情况下检测SwiftUI的touchDown事件?

31

我正在尝试在SwiftUI中检测手指何时首次接触视图。使用UIKit事件可以很容易地完成此操作,但是在SwiftUI中无法解决此问题。

我尝试了DragGesture并将最小移动距离设置为0,但它仍然不会改变,直到手指移动。

TapGesture仅在提起手指时起作用,而LongPressGesture无论我将参数设置为多少都无法快速触发。

DragGesture(minimumDistance: 0, coordinateSpace: .local).onChanged({ _ in print("down")})

LongPressGesture(minimumDuration: 0.01, maximumDistance: 100).onEnded({_ in print("down")})

我希望能够在手指接触视图时立即检测到touchDown事件。苹果公司的默认手势对距离或时间有限制。

更新:这不再是一个问题,因为苹果似乎已经更新了DragGesture的工作方式,或者可能我遇到了特定的上下文错误。


必须在TapGesture上使用.updating修饰符,但是我不知道如何做到这一点。 - Pbk
这里有一个非常优雅的解决方案供您参考: https://serialcoder.dev/text-tutorials/swiftui/handle-press-and-release-events-in-swiftui/ - allenlinli
10个回答

47
你可以像这样使用 .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 属性包装器在手势结束时自动将其值重置为原始值。这样,您只需要担心设置 isTappedtrue。当交互结束时,它将自动重新变为 false
  • updating 修饰符有一个带有三个参数的奇怪闭包。在这种情况下,我们只对中间的参数感兴趣。它是 GestureState 包装值的 inout 参数,所以我们可以在这里设置它。第一个参数具有手势的当前值;第三个参数是包含一些动画上下文的 Transaction

2
@eli_slade 我认为这个答案应该被接受为最佳答案。 - Schiopu Evgheni
同样,零最小距离是我在寻找解决方案时最缺少的重要因素。 - Peter Schumacher
1
这个代码完美运行。但是我不确定为什么还需要调用 .onChanged.onEnded,即使它们什么也不做,否则 updating 方法只会被调用一次并立即将 isTapped 设置为 false。在 Xcode 14 和 iOS 16.0 上进行了测试。 - mrfour
对我来说没有起作用 :( - ScottyBlades

13
您可以通过以下方式创建视图修饰符:
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!")
            }
    }
}

这个代码完美运行,我很喜欢。但是感觉和onTapGesture一样。 - user3069232
同时手势是唯一对我有效的按下和抬起的方法。谢谢! - ScottyBlades

10

如果您将这两个问题的代码结合起来:

如何在SwiftUI中检测点击手势的位置?

UITapGestureRecognizer-使其在触摸下按下而不是松开时工作?

您可以制作出类似这样的东西:

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手势,但这只是一个开始。希望苹果在某个时候内置对此的支持。


9
你所需的仅是这个:
LongPressGesture().onChanged {_ in print("down") }

这是一个演示应用程序:

App Demo

以下是其代码:

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()})
    }
}

简单而有效,非常好的答案! - marticztn
对我没用。 - ScottyBlades
1
不是一个合适的解决方案。 当用户从视图中抬起手指时,状态不会改变。 - undefined

5
这是一种检测状态变化以及触摸坐标(在此示例中为Text View)的解决方案:
我添加了一个枚举来管理状态(使用“began”,“moved”和“ended”来代表那些怀旧的UIKit)。

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)"
    }
}

1
这也是很好的代码。但它有相同的问题。它检测到了触摸,但直到我移开手指才报告它,这与轻敲相同。 - user3069232
它目前可以检测到触摸,并立即报告。也许我在您的评论中漏掉了什么...... 我正在iPhone XS上测试,使用的是iOS 13。 - eharo2
我再检查一下。在我抬起手指之前,似乎它没有起作用。 - user3069232
是的,当我按下按钮时,onChanged和onEnded没有被调用。这个解决方案对我不起作用。 - ScottyBlades

4

以上都不完美,比如DragGesture使得表格无法滚动

经过长时间搜索,我找到了这个圣杯!

注意:它是公开可用的,但前缀 _ 意味着它可能不适合生产环境。

但仍然非常好用。

._onButtonGesture(pressing: { pressing in
                    
}, perform: {

})

1
有些东西可以直接拿来用,而不必重新发明轮子。我喜欢这个想法。谢谢! - ScottyBlades
太完美了!正是我所需要的! - undefined
刚刚发布了一个包含这段代码的watchOS应用,并且已经通过了App Store的审核。 - undefined
是的,已经发布和批准了几个应用程序,而且还使用了这个公共方法。 - undefined

3
对于iOS系统,这里有一个解决方案可以检测所有手势状态。使用的代码取自Joe在此问题下的回答
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
    }
}

3

我已经在这里回答过了,但也值得在这个更受欢迎的问题上发布。


你可以在View上使用隐藏的_onButtonGesture方法,它是公共的,与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")")
        }
    }
}

结果:

Result

由于帧速率不佳,作为GIF看起来并不理想,但每当您按下时,pressing变为true,然后在释放时变为false


谢谢!这正如预期的那样工作。不知道为什么苹果还没有公开它。然而,我担心使用它会导致苹果拒绝该应用程序。 :/ - Revanth Kausikan
1
@RevanthKausikan 尽管它是未定义的行为,API 可能会在任何时候被删除 - 但我认为你不太可能因此被拒绝。很可能是公开的以进行内联处理(这意味着该函数在常规应用程序中被调用,而不是直接使用它)。我不知道内部工作原理,这只是推测。 - George

1
这是一个针对watchOS的特别回答,因为在iOS中定义 isTouchedDown 的内容会以奇怪的方式被多次调用。如果你想在触摸视图时运行操作,在iOS上有更好的解决方案,比如我的另一个答案
注意:当容器屏幕出现时,isTouchedDown定义仍将触发几次。防止这种情况的一个巧妙解决方案是,在onAppear中0.3秒后将布尔值shouldCallIsTouchedDownCode设置为false。保留HTML标签。
@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
             }
        }
}

1
这是一个复制粘贴。抱歉。但(作为一个开端)对我来说效果不错!
来源于https://betterprogramming.pub/implement-touch-events-in-swiftui-b3a2b0700fd4
struct ContentView: View {
    @State var buttonPressed = false
    var body: some View {
        Text("Press to accelerate!")
            .foregroundColor(Color.white)
            .padding(10)
            .background(Color.gray)
            .cornerRadius(6)
            .padding(10)
            .scaleEffect(buttonPressed ? 0.8 : 1)
            .animation(.spring(), value: buttonPressed)

            /// Apply the method.
            .onTouchDownUp { pressed in
                self.buttonPressed = pressed
            }
    }
}

extension View {
    /// A convenience method for applying `TouchDownUpEventModifier.`
    func onTouchDownUp(pressed: @escaping ((Bool) -> Void)) -> some View {
        self.modifier(TouchDownUpEventModifier(pressed: pressed))
    }
}

struct TouchDownUpEventModifier: ViewModifier {
    /// Keep track of the current dragging state. To avoid using `onChange`, we won't use `GestureState`
    @State var dragged = false

    /// A closure to call when the dragging state changes.
    var pressed: (Bool) -> Void
    func body(content: Content) -> some View {
        content
            .gesture(
                DragGesture(minimumDistance: 0)
                    .onChanged { _ in
                        if !dragged {
                            dragged = true
                            pressed(true)
                        }
                    }
                    .onEnded { _ in
                        dragged = false
                        pressed(false)
                    }
            )
    }
}

我需要做的是将.gesture(更改为.simultaneousGesture(,因为我在同一视图上还有另一个DragGesture。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接