【问题标题】:Adding a drag gesture in SwiftUI to a View inside a ScrollView blocks the scrolling将 SwiftUI 中的拖动手势添加到 ScrollView 内的 View 会阻止滚动
【发布时间】:2020-01-02 03:40:27
【问题描述】:

所以我有一个ScrollView 持有一组观点:

    ScrollView {
        ForEach(cities) { city in
            NavigationLink(destination: ...) {
                CityRow(city: city)
            }
            .buttonStyle(BackgroundButtonStyle())
        }
    }

在每个视图中我都有一个拖动手势:

    let drag = DragGesture()
        .updating($gestureState) { value, gestureState, _ in
            // ...
        }
        .onEnded { value in
            // ...
        }

我分配给视图的一部分:

    ZStack(alignment: .leading) {
        HStack {
            // ...
        }
        HStack {
            // ...
        }
        .gesture(drag)
    }

只要我附加手势,ScrollView 就会停止滚动。让它滚动它的唯一方法是从它没有附加手势的部分开始滚动。我怎样才能避免它并使两者一起工作。在 UIKit 中就像在 shouldRecognizeSimultaneouslyWith 方法中指定 true 一样简单。我怎样才能在 SwiftUI 中拥有相同的功能?

在 SwiftUI 中,我尝试使用 .simultaneousGesture(drag).highPriorityGesture(drag) 附加手势——它们的工作原理都与 .gesture(drag) 相同。我还尝试为including: 参数提供所有可能的静态GestureMask 值——我要么滚动工作,要么我的拖动手势工作。两者都不要。

这是我使用拖动手势的目的:

【问题讨论】:

  • 这有点奇怪。如果我将simultaneousGesture 添加到ScrollView - 它可以很好地与ScrollView 儿童手势配合使用。但是ScrollView 滚动不起作用。
  • 嗨@zh,你找到解决这个问题的方法了吗?
  • 我在手表应用上遇到了同样的问题,当拖动手势添加到视图时,滚动视图不会滚动...

标签: swiftui ios13


【解决方案1】:

您可以将 minimumDistance 设置为某个值(例如 30)。 那么拖动只有在水平拖动并达到最小距离时才有效,否则滚动视图或列表手势会覆盖视图手势

.gesture(DragGesture(minimumDistance: 30, coordinateSpace: .local)

【讨论】:

    【解决方案2】:

    就在之前

    .gesture(drag)
    

    你可以添加

    .onTapGesture { }
    

    这对我有用,显然添加一个 tapGesture 可以避免两个 DragGesture 之间的混淆。

    希望对你有帮助

    【讨论】:

    • 值得注意的是,这种方法效果很好,但不幸的是会导致点击手势劫持——尤其是在使用 UIKit 包装的组件时。我一直使用它,直到我尝试点击作为 SwiftUI 组件包装的 UITableViewCell - 才发现此方法劫持了 UIKit 元素的 tapGesture。如果您的应用程序中不使用 UIKit 元素,我会推荐这种方法 - 但如果您是,那么我发现 Mac3n 的解决方案更可靠
    【解决方案3】:

    我根据 Michel 的回答创建了一个易于使用的扩展。

    struct NoButtonStyle: ButtonStyle {
        func makeBody(configuration: Configuration) -> some View {
            configuration.label
        }
    }
    
    extension View {
        func delayTouches() -> some View {
            Button(action: {}) {
                highPriorityGesture(TapGesture())
            }
            .buttonStyle(NoButtonStyle())
        }
    }
    

    您在使用拖动手势后应用它。

    例子:

    ScrollView {
        YourView()
            .gesture(DragGesture(minimumDistance: 0)
                .onChanged { _ in }
                .onEnded { _ in }
            )
            .delayTouches()
    }
    

    【讨论】:

    • 我发现这确实有效并且有助于在 ScrollView 上进行拖动手势,但值得注意的是,这种方法可以阻止与 ScrollView 相同的视图上的点击手势/按钮按下
    • 在列表中使用自定义滑块非常适合我 - 很好的解决方法!
    【解决方案4】:

    我终于找到了一个似乎对我有用的解决方案。我发现Button 是神奇的生物。它们会正确传播事件,即使您在 ScrollViewList 内也能继续工作。

    现在,你会说

    是的,但是米歇尔,我不想要一个点击后产生某些效果的该死按钮,我想要长按某些东西,或者拖动某些东西。

    很公平。但是,如果您知道如何做事,您必须将知识的Button 视为实际上使label: 下的所有内容都正常工作的东西!因为 Button 实际上会尝试表现,并将其手势委托给下面的控件(如果它们实际上实现了onTapGesture),因此您可以获得一个切换按钮或一个可以点击内部的info.circle 按钮。换句话说,出现在onTapGesture {} 之后的所有手势(但不是之前的手势)都可以使用。

    作为一个复杂的代码示例,你必须拥有如下:

    ScrollView {
        Button(action: {}) {        // Makes everything behave in the "label:"
            content                 // Notice this uses the ViewModifier ways ... hint hint
                .onTapGesture {}    // This view overrides the Button
                .gesture(LongPressGesture(minimumDuration: 0.01)
                    .sequenced(before: DragGesture(coordinateSpace: .global))
                    .updating(self.$dragState) { ...
    

    这个例子使用了一个复杂的手势,因为我想证明它们确实有效,只要那个难以捉摸的Button/onTapGesture 组合在那里。

    现在您会注意到这并不完全完美,在将长按委托给您之前,长按实际上也被按钮长按(因此该示例将有超过 0.01 秒的长按)。此外,如果您想删除按下的效果,您必须拥有ButtonStyle。换句话说,YMMV,很多测试,但对于我自己的使用,这是我能够在项目列表中进行实际长按/拖动工作的最接近的。

    【讨论】:

    • 查看 Stack Overflow 的其他帖子,我发现这个答案确实适用并且使用了与我相同的技术:stackoverflow.com/a/58643879/1060383
    • 这是一个很棒的发现,谢谢!我一直在将手势识别器附加到Button(和NavigationLink,基本相同),但我没有尝试先添加onTapGesture。我必须尝试它是否适用于我的情况,如果可以,请选择您的答案作为正确的答案。
    • @zh.只是想与您核实一下您是否找到了更好的解决方案,或者对我的解决方案进行了改进。
    【解决方案5】:

    我找不到一个纯粹的 SwiftUI 解决方案,所以我使用 UIViewRepresentable 作为解决方法。与此同时,我已经向 Apple 提交了一个错误。基本上,我已经创建了一个带有平移手势的清晰视图,我将在我想要添加手势的任何 SwiftUI 视图上呈现它。这不是一个完美的解决方案,但也许对你来说已经足够了。

    public struct ClearDragGestureView: UIViewRepresentable {
        public let onChanged: (ClearDragGestureView.Value) -> Void
        public let onEnded: (ClearDragGestureView.Value) -> Void
    
        /// This API is meant to mirror DragGesture,.Value as that has no accessible initializers
        public struct Value {
            /// The time associated with the current event.
            public let time: Date
    
            /// The location of the current event.
            public let location: CGPoint
    
            /// The location of the first event.
            public let startLocation: CGPoint
    
            public let velocity: CGPoint
    
            /// The total translation from the first event to the current
            /// event. Equivalent to `location.{x,y} -
            /// startLocation.{x,y}`.
            public var translation: CGSize {
                return CGSize(width: location.x - startLocation.x, height: location.y - startLocation.y)
            }
    
            /// A prediction of where the final location would be if
            /// dragging stopped now, based on the current drag velocity.
            public var predictedEndLocation: CGPoint {
                let endTranslation = predictedEndTranslation
                return CGPoint(x: location.x + endTranslation.width, y: location.y + endTranslation.height)
            }
    
            public var predictedEndTranslation: CGSize {
                return CGSize(width: estimatedTranslation(fromVelocity: velocity.x), height: estimatedTranslation(fromVelocity: velocity.y))
            }
    
            private func estimatedTranslation(fromVelocity velocity: CGFloat) -> CGFloat {
                // This is a guess. I couldn't find any documentation anywhere on what this should be
                let acceleration: CGFloat = 500
                let timeToStop = velocity / acceleration
                return velocity * timeToStop / 2
            }
        }
    
        public class Coordinator: NSObject, UIGestureRecognizerDelegate {
            let onChanged: (ClearDragGestureView.Value) -> Void
            let onEnded: (ClearDragGestureView.Value) -> Void
    
            private var startLocation = CGPoint.zero
    
            init(onChanged: @escaping (ClearDragGestureView.Value) -> Void, onEnded: @escaping (ClearDragGestureView.Value) -> Void) {
                self.onChanged = onChanged
                self.onEnded = onEnded
            }
    
            public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
                return true
            }
    
            @objc func gestureRecognizerPanned(_ gesture: UIPanGestureRecognizer) {
                guard let view = gesture.view else {
                    Log.assertFailure("Missing view on gesture")
                    return
                }
    
                switch gesture.state {
                case .possible, .cancelled, .failed:
                    break
                case .began:
                    startLocation = gesture.location(in: view)
                case .changed:
                    let value = ClearDragGestureView.Value(time: Date(),
                                                           location: gesture.location(in: view),
                                                           startLocation: startLocation,
                                                           velocity: gesture.velocity(in: view))
                    onChanged(value)
                case .ended:
                    let value = ClearDragGestureView.Value(time: Date(),
                                                           location: gesture.location(in: view),
                                                           startLocation: startLocation,
                                                           velocity: gesture.velocity(in: view))
                    onEnded(value)
                @unknown default:
                    break
                }
            }
        }
    
        public func makeCoordinator() -> ClearDragGestureView.Coordinator {
            return Coordinator(onChanged: onChanged, onEnded: onEnded)
        }
    
        public func makeUIView(context: UIViewRepresentableContext<ClearDragGestureView>) -> UIView {
            let view = UIView()
            view.backgroundColor = .clear
    
            let drag = UIPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.gestureRecognizerPanned))
            drag.delegate = context.coordinator
            view.addGestureRecognizer(drag)
    
            return view
        }
    
        public func updateUIView(_ uiView: UIView,
                                 context: UIViewRepresentableContext<ClearDragGestureView>) {
        }
    }
    

    【讨论】:

    • ZStacking this over a view 仍然会劫持它的手势。
    【解决方案6】:

    很高兴看到 iOS 15 通过易于使用的 API 将期待已久的 .swipeActions 视图修饰符带到 SwiftUI 中的 List

    基于原始问题的示例代码:

    List {
            ForEach(cities) { city in
                NavigationLink(destination: ...) {
                    CityRow(city: city)
                }
                .buttonStyle(BackgroundButtonStyle())
                .swipeActions(edge: .trailing, allowFullSwipe: true) {
                   Button(role: .destructive) {
                        // call delete method
                   } label: {
                        Label("Delete", systemImage: "trash")
                   }
                   Button {
                        // call flag method
                   } label: {
                        Label("Flag", systemImage: "flag")
                   }
                }
            }
        }
    

    动作按列出的顺序出现,从起始边向内开始。

    上面的例子产生:

    请注意,swipeActions 会覆盖 onDelete 处理程序(如果在 ForEach 上提供)

    Read more in Apple's developer docs

    【讨论】:

    • 在可分页的 TabView 内滑动操作时不起作用:)
    【解决方案7】:

    我试图在我的应用程序中实现类似的列表样式,但发现手势与 ScrollView 冲突。在花费数小时研究并尝试解决此问题的可能修复和解决方法后,截至 XCode 11.3.1,我认为这是 Apple 需要在未来版本的 SwiftUI 中解决的错误。

    包含用于复制该问题的示例代码的 Github 存储库已放在一起 here,并已通过参考 FB7518403 报告给 Apple。

    希望它尽快修复!

    【讨论】:

      【解决方案8】:

      我在拖动滑块时遇到了类似的问题:

      stackoverflow question

      这是有效的答案代码,带有“DispatchQueue.main.asyncAfter”的“技巧”

      也许你可以为你的 ScrollView 尝试类似的东西。

      struct ContentView: View {
      @State var pos = CGSize.zero
      @State var prev = CGSize.zero
      @State var value = 0.0
      @State var flag = true
      
      var body: some View {
          let drag = DragGesture()
              .onChanged { value in
                  self.pos = CGSize(width: value.translation.width + self.prev.width, height: value.translation.height + self.prev.height)
          }
          .onEnded { value in
              self.pos = CGSize(width: value.translation.width + self.prev.width, height: value.translation.height + self.prev.height)
              self.prev = self.pos
          }
          return VStack {
              Slider(value: $value, in: 0...100, step: 1) { _ in
                  self.flag = false
                  DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                      self.flag = true
                  }
              }
          }
          .frame(width: 250, height: 40, alignment: .center)
          .overlay(RoundedRectangle(cornerRadius: 25).stroke(lineWidth: 2).foregroundColor(Color.black))
          .offset(x: self.pos.width, y: self.pos.height)
          .gesture(flag ? drag : nil)
      }
      

      }

      【讨论】:

      • 我已经尝试过这种方法。我为ScrollView 添加了另一个手势simultaneousGesture,计算拖动方向并允许或不允许子视图的滑动手势。但是这种方法有一个问题。如果我取消所有拖动手势,则 ScrollView 滚动不会开始,只有在手势开始之前取消这些手势才会开始。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-02-10
      • 1970-01-01
      相关资源
      最近更新 更多