Search code examples
iosswiftswiftuigesture

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

Solution

  • 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.