I think the velocity would be useful to be pt/s since UIPanGestureRecognizer is.
Mostly gesture velocity projects into the animation's initial velocity.
If we move a view 200pt with 100pt/s, we set a value of (100/200) as an initial velocity.
So I decided to use the time of DragGesture.View
to get diff as a timeline.
It takes a little bit of messy code, such as retaining previous value.
Then I created a modifier that shortens it.
@propertyWrapper
public struct GestureVelocity: DynamicProperty {
@State private var previous: DragGesture.Value?
@State private var current: DragGesture.Value?
func update(_ value: DragGesture.Value) {
if current != nil {
previous = current
}
current = value
}
func reset() {
previous = nil
current = nil
}
public init() {
}
public var projectedValue: GestureVelocity {
return self
}
public var wrappedValue: CGVector {
value
}
private var value: CGVector {
guard
let previous,
let current
else {
return .zero
}
let timeDelta = current.time.timeIntervalSince(previous.time)
let speedY = Double(
current.translation.height - previous.translation.height
) / timeDelta
let speedX = Double(
current.translation.width - previous.translation.width
) / timeDelta
return .init(dx: speedX, dy: speedY)
}
}
extension Gesture where Value == DragGesture.Value {
public func updatingVelocity(_ velocity: GestureVelocity) -> _EndedGesture<_ChangedGesture<Self>> {
onChanged { value in
velocity.update(value)
}
.onEnded { _ in
velocity.reset()
}
}
}
struct Joystick: View {
@State private var position: CGSize = .zero
// ✅
@GestureVelocity private var velocity: CGVector
var body: some View {
stick
.padding(10)
}
private var stick: some View {
Circle()
.fill(Color.blue)
.offset(position)
.gesture(
DragGesture(
minimumDistance: 0,
coordinateSpace: .local
)
.onChanged({ value in
position = value.translation
})
.onEnded({ value in
// here
let velocity = self.velocity
})
.updatingVelocity($velocity) // ✅ make sure using after onEnded
)
}
}