您如何检测没有移动或持续时间的 SwiftUI touchDown 事件?

Posted

技术标签:

【中文标题】您如何检测没有移动或持续时间的 SwiftUI touchDown 事件?【英文标题】:How do you detect a SwiftUI touchDown event with no movement or duration? 【发布时间】:2019-11-10 00:06:38 【问题描述】:

我试图检测手指何时第一次接触 SwiftUI 中的视图。我可以使用 UIKit Events 轻松做到这一点,但在 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 的工作方式,或者我可能遇到了特定的上下文错误。

【问题讨论】:

必须在 TapGesture 上加上 .updating 修饰符,但不知道怎么做。 【参考方案1】:

您可以像这样使用.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。交互结束时会自动再次变为falseupdating 修饰符有一个带有三个参数的奇怪闭包。在这种情况下,我们只对中间那个感兴趣。这是GestureState 的包装值的inout 参数,所以我们可以在这里设置它。第一个参数是手势的当前值;第三个是Transaction,包含一些动画上下文。

【讨论】:

@eli_slade 这个答案应该被接受为最佳答案恕我直言 同样,最小距离为零是我在寻找解决方案时缺少的最重要的东西。【参考方案2】:

如果你结合这两个问题的代码:

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 会在某个时候对此提供支持。

【讨论】:

【参考方案3】:

您可以这样创建视图修饰符:

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 一样。【参考方案4】:

其实,@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())
    

【讨论】:

【参考方案5】:

这是一种检测状态之间变化以及触摸坐标(在本例中为Text View)的解决方案:

我添加了一个枚举来管理状态(对那些 UIKit-nostalgic 使用开始、移动和结束)

导入 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)"
    

【讨论】:

这也是不错的代码。但它有同样的问题。我检测到触地得分,但直到我移除触摸后才报告它,这与点击相同。 它现在确实检测到触摸,并立即报告。我可能在您的评论中遗漏了一些东西......我正在使用 ios 13 的 iPhone XS 进行测试 我会再检查一次。在我把手指从屏幕上移开之前,我觉得它不起作用。【参考方案6】:

对于 iOS,这是一个检测所有手势状态的解决方案。使用来自Joe's answer under this question 的代码。

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
    

【讨论】:

【参考方案7】:

这是一个watchOS 特定的答案,因为isTouchedDown 定义中的内容在iOS 上以一种奇怪的方式被多次调用。如果你想在你的视图被向上/向下触摸时运行动作,iOS 有更好的解决方案,例如my other answer。


注意:当容器屏幕出现时,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
             
        

【讨论】:

【参考方案8】:

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


您可以在公开的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")")
        
    

结果:

由于帧速率的原因,它作为 GIF 看起来不太好,但每当你按下pressing 时,就会转到true,然后在发布时false

【讨论】:

以上是关于您如何检测没有移动或持续时间的 SwiftUI touchDown 事件?的主要内容,如果未能解决你的问题,请参考以下文章

检测在 SwiftUI 中点击的系统警报按钮

您如何在移动 safari 上检测 3G 与 Wifi 连接?

如何使用 SwiftUI 和 Combine 检测 Datepicker 的值变化?

(SwiftUI, MacOS 11.0) 如何在 SecureTextFeild 中检测焦点?

如何摆脱不需要的 SwiftUI 动画?

git 检测您的 Java 项目中的重命名/移动效果不佳 - 该怎么办?