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)"
}
)
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.
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:)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With