Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect DragGesture cancelation in SwiftUI

So I have a Rectangle with an added DragGesture and want to track gesture start, change and ending. The issue is when I put another finger on the Rectangle while performing the gesture, the first gesture stop calling onChange handler and does not fire onEnded handler. Also the handlers doesn't fire for that second finger.

But if I place third finger without removing previous two the handlers for that gesture start to fire (and so on with even presses cancel out the odd ones)

Is it a bug? Is there a way to detect that the first gesture was canceled?

Rectangle()
  .fill(Color.purple)
  .gesture(
    DragGesture(minimumDistance: 0, coordinateSpace: .local)
      .onChanged() { event in
        self.debugLabelText = "changed \(event)"
      }
      .onEnded() { event in
        self.debugLabelText = "ended \(event)"
      }
  )
like image 534
whn Avatar asked Nov 11 '19 19:11

whn


2 Answers

Thanks to @krjw for the hint with an even number of fingers

This appears to be a problem in the Gesture framework for attempting to detect a bunch of gestures even if we didn't specify that it should be listening for them.

As the documentation is infuriatingly sparse we can only really guess at what the intended behaviour and lifecycle here is meant to be (IMHO - this seems like a bug) - but it can be worked around.

Define a struct method like

func onDragEnded() {
  // set state, process the last drag position we saw, etc
}

Then combine several gestures into one to cover the bases that we didn't specify

let drag = DragGesture(minimumDistance: 0)
      .onChanged({ drag in
       // Do stuff with the drag - maybe record what the value is in case things get lost later on 
      })
      .onEnded({ drag in
        self.onDragEnded()
      })

     let hackyPinch = MagnificationGesture(minimumScaleDelta: 0.0)
      .onChanged({ delta in
        self.onDragEnded()
      })
      .onEnded({ delta in
        self.onDragEnded()
      })

    let hackyRotation = RotationGesture(minimumAngleDelta: Angle(degrees: 0.0))
      .onChanged({ delta in
        self.onDragEnded()
      })
      .onEnded({ delta in
        self.onDragEnded()
      })

    let hackyPress = LongPressGesture(minimumDuration: 0.0, maximumDistance: 0.0)
      .onChanged({ _ in
        self.onDragEnded()
      })
      .onEnded({ delta in
        self.onDragEnded()
      })

    let combinedGesture = drag
      .simultaneously(with: hackyPinch)      
      .simultaneously(with: hackyRotation)
      .exclusively(before: hackyPress)

/// The pinch and rotation may not be needed - in my case I don't but 
///   obviously this might be very dependent on what you want to achieve

There might be a better combo for simultaneously and exclusively but for my use case at least (which is for something similar to a joystick) this seems like it is doing the job

There is also a GestureMask type that might have done the job but there is no documentation on how that works.

like image 185
Andrew Lipscomb Avatar answered Oct 21 '22 21:10

Andrew Lipscomb


One solution is to use a @GestureState property that tracks if the drag is currently running. The state will be reset to false automatically when the gesture is cancelled.

struct DragSampleView: View {
    @GestureState private var dragGestureActive: Bool = false
    @State var dragOffset: CGSize = .zero

    var draggingView: some View {
        Text("DRAG ME").padding(50).background(.red)
    }

    var body: some View {
        ZStack {
            Color.blue.ignoresSafeArea()
            draggingView
                .offset(dragOffset)
                .gesture(DragGesture()
                    .updating($dragGestureActive) { value, state, transaction in
                        state = true
                    }
                    .onChanged { value in
                        print("onChanged")
                        dragOffset = value.translation
                    }.onEnded { value in
                        print("onEnded")
                        dragOffset = .zero
                    })
                .onChange(of: dragGestureActive) { newIsActiveValue in
                    if newIsActiveValue == false {
                        dragCancelled()
                    }
                }
        }
    }

    private func dragCancelled() {
        print("dragCancelled")
        dragOffset = .zero
    }
}

struct DragV_PreviewProvider: PreviewProvider {
    static var previews: some View {
        DragSampleView()
    }
}

See https://developer.apple.com/documentation/swiftui/draggesture/updating(_:body:)

like image 27
Klaas Avatar answered Oct 21 '22 21:10

Klaas