【问题标题】:How do you detect a SwiftUI touchDown event with no movement or duration?您如何检测没有移动或持续时间的 SwiftUI touchDown 事件?
【发布时间】: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 修饰符,但不知道怎么做。

标签: swift swiftui gesture


【解决方案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。交互结束时会自动再次变为false
  • updating 修饰符有一个带有三个参数的奇怪闭包。在这种情况下,我们只对中间那个感兴趣。这是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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-08-08
            • 1970-01-01
            • 1970-01-01
            • 2011-03-14
            • 1970-01-01
            相关资源
            最近更新 更多