在SwiftUI中向ScrollView内的View添加拖动手势会阻止滚动

时间:2019-08-28 21:41:52

标签: swiftui ios13

所以我有一个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中,就像在true方法中指定shouldRecognizeSimultaneouslyWith一样简单。在SwiftUI中我该怎么办?

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

这是我正在使用的拖动手势: enter image description here

9 个答案:

答案 0 :(得分:17)

您可以将minimumDistance设置为某个值(例如30)。 然后,仅当您水平拖动并达到最小距离时,拖动才起作用,否则,滚动视图或列表手势会覆盖视图手势

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

答案 1 :(得分:10)

就在

之前
.gesture(drag)

您可以添加

.onTapGesture { }

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

我希望这会有所帮助

答案 2 :(得分:8)

我根据米歇尔的答案创建了一个易于使用的扩展程序。

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

答案 3 :(得分:2)

我试图在我的应用程序中实现类似的列表样式,只是发现手势与ScrollView冲突。从XCode 11.3.1开始,花了数小时研究并尝试解决此问题的方法和变通办法之后,我相信这是Apple需要在以后的SwiftUI版本中解决的错误。

一个带有示例代码的Github存储库可以复制该问题,here汇总在一起,并已向苹果公司报告,引用编号为FB7518403

希望它能尽快修复!

答案 4 :(得分:1)

很高兴看到 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")
               }
            }
        }
    }

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

上面的例子产生:

swipe actions

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

Read more in Apple's developer docs

答案 5 :(得分:0)

我相信您想要的是 simultaneousGesture(_:including:),该文件已记录在here中。

HStack {
    // ...
}
.simultaneousGesture(drag)

答案 6 :(得分:0)

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

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

}

答案 7 :(得分:0)

我找不到针对此的纯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>) {
    }
}

答案 8 :(得分:0)

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

现在,您会说

  

是的,但米歇尔,我不希望使用带有某些效果的friggin按钮,而是要长按某些内容或拖动某些内容。

足够公平。但是,您必须将Button的知识视为可以使label:下的所有内容真正正常工作的东西,如果您知道该怎么做!因为Button实际上会尝试执行操作,并将其手势委派给下面的控件(如果它们确实实现了onTapGesture),因此您可以在其中点击以获取切换或info.circle按钮。换句话说,在onTapGesture {}之后出现的 All 手势(而不是之前的手势)将起作用。

作为一个复杂的代码示例,您必须具备以下条件:

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需要进行大量测试,但是对于我自己的用途,这是我能够在项目列表中进行长按/拖动的最接近的功能。