Search code examples
swiftswiftui

Gesture onEnded doesn't fire in scroll view


I have a basic scroll view in SwiftUI that has stackable cards. The cards can be switched by swiping on them. Sometimes scrolls on the scroll view can be mistaken as drags on the draggable view. In this case the top card is dragged to somewhere else on the screen, but the onEnded doesn't fire and the card remains stuck in that position without returning back to the stack.

How can I fix the issue where scrolls on the scroll view are mistaken as drags on this sub view?

import SwiftUI

struct tetfqwe: View {
    @State var cards: [String] = ["1", "2", "3", "4", "5"]
    @State var offset: CGSize = .zero
    
    var body: some View {
        ScrollView {
            Color.blue.frame(height: 200)
            LazyVStack {
                ZStack {
                    ForEach(cards.indices, id: \.self) { index in
                        Color.red.frame(width: 120, height: 90).cornerRadius(15, corners: .allCorners)
                            .overlay(content: {
                                RoundedRectangle(cornerRadius: 15)
                                    .stroke(.black, lineWidth: 1.0)
                            })
                            .overlay(content: {
                                Text("\(index)")
                            })
                            .onTapGesture {
                                UIImpactFeedbackGenerator(style: .light).impactOccurred()
                            }
                            .offset(x: Double(((cards.count - 1) - index) * 3))
                            .offset(x: index == (cards.count - 1) ? offset.width : 0.0, y: index == (cards.count - 1) ? offset.height : 0.0)
                            .highPriorityGesture(DragGesture()
                                .onChanged({ value in
                                    if cards.count > 1 {
                                        offset = value.translation
                                    }
                                })
                                    .onEnded({ value in
                                        if cards.count > 1 {
                                            if abs(value.translation.width) > 45.0 {
                                                ended()
                                            } else {
                                                withAnimation(.bouncy(duration: 0.3)){
                                                    offset = .zero
                                                }
                                            }
                                        }
                                    })
                            )
                    }
                }
            }
            Color.orange.frame(height: 1000).padding(.top, 100)
            Color.green.frame(height: 1000)
            Color.gray.frame(height: 1000)
        }
    }
    func ended(){
        withAnimation(.bouncy(duration: 0.3)){
            let element = cards.removeLast()
            cards.insert(element, at: 0)
            offset = .zero
        }
    }
}

#Preview {
    tetfqwe()
}

Solution

  • Do this on top side of your struct:

    struct tetfqwe: View {
    
    @State var cards: [String] = ["1", "2", "3", "4", "5"]
    @State var offset: CGSize = .zero
    @State var shouldScroll: Bool // add this to your code
            
    var axes: Axis.Set { // add this to your code
        return shouldScroll ? .vertical : []
    }
    

    And towards the bottom, do this:

        .highPriorityGesture(DragGesture()
            .onChanged({ value in
                
                shouldScroll = false // add this to your code
    
                if cards.count > 1 {
                    offset = value.translation
                }
                     })
                .onEnded({ value in
     
                    shouldScroll = true // add this to your code
    
                    if cards.count > 1 {
    

    Cards Drag Demo

    This solution simply disables the scrollView while the drag is in action '.onChanged' and re-enables it when drag gesture 'onEnded' ends.