DragGesture + ScrollView = problems. Any good workarounds?

When placing a DragGesture on a view containing a ScrollView, dragging within the ScrollView causes onChanged to trigger, while onEnded does not trigger. Does anybody have a workaround for this?


struct ContentView: View {
     @State var offset: CGSize = .zero
     var body: some View {
          ZStack {
          Spacer()
          Color.clear


          if self.offset != .zero {
               Color.blue.opacity(0.25)
          }


          VStack {
               Color.gray.frame(height: 44.0)
               ScrollView {
                    ForEach(0..<100, id: \.self) { _ in
                         Text("Don't move please")
                    }
                   }
               Color.gray.frame(height: 44.0)
           }
          .frame(width: 320.0, height: 568.0)
          .offset(self.offset)
          .animation(.easeInOut)
          .gesture( DragGesture(minimumDistance: 10.0, coordinateSpace: .global)
          .onChanged { (value) in
               self.offset = value.translation
          }
          .onEnded { (_) in
               self.offset = .zero
          })
      }
   }
}


I know I could place gestures on the top/bottom gray areas (they represent bars), but in my actual app the owner of the drag gesture is a container that knows nothing about its contents.

I've run into two workarounds that are not ideal but accomplish the goal.
  1. Sequence a long press gesture before the drag gesture. longPress.sequenced(before: drag)

  2. Add an empty onTapGesture *before* the drag gesture.

Work around I did was

  1. I used a ScrollView where in I had a state if it is currently scrolling
  2. instead of DragGesture's .onChanged(), I used .updating() (this seems to be ran first before onChanged)
  3. I checked the ScrollView's scrolling state, if it's being scrolled, don't update the View's drag offset else update it.

What you observe is that your gesture is being cancelled. From Adding Interactivity with Gestures - Apple Developer article:

SwiftUI only invokes the onEnded(_:) callback when the gesture succeeds.

This is what works for me:

struct DragGestureViewModifier: ViewModifier {
    @GestureState private var isDragging: Bool = false
    @State var gestureState: GestureStatus = .idle

    var onStart: Callback?
    var onUpdate: ((DragGesture.Value) -> Void)?
    var onEnd: Callback?
    var onCancel: Callback?

    func body(content: Content) -> some View {
        content
            .gesture(
                DragGesture()
                    .updating($isDragging) { _, isDragging, _ in
                        isDragging = true
                    }
                    .onChanged(onDragChange(_:))
                    .onEnded(onDragEnded(_:))
            )
            .onChange(of: gestureState) { state in
                guard state == .started else { return }
                gestureState = .active
            }
            .onChange(of: isDragging) { value in
                if value, gestureState != .started {
                    gestureState = .started
                    onStart?()
                } else if !value, gestureState != .ended {
                    gestureState = .cancelled
                    onCancel?()
                }
            }
    }

    func onDragChange(_ value: DragGesture.Value) {
        guard gestureState == .started || gestureState == .active else { return }
        onUpdate?(value)
    }

    func onDragEnded(_ value: DragGesture.Value) {
        gestureState = .ended
        onEnd?()
    }

    enum GestureStatus: Equatable {
        case idle
        case started
        case active
        case ended
        case cancelled
    }
}
DragGesture + ScrollView = problems. Any good workarounds?
 
 
Q