7

I would like to extract the velocity of a DragGesture to use for the initialVelocity parameter of a spring animation. I am trying to create a movable card that has a responsive spring animation like that found in the Apple Maps app.

I have tried calculating velocity by dividing the height of the drag translation by the total time elapsed of the drag gesture.

v_abs = abs(Double(drag.translation.height / CGFloat(drag.time.timeIntervalSinceNow)))

The problem is that when a user begins a drag they may slow down the drag before flicking and releasing, which causes the velocity to be very slow since a long period of time has elapsed. If possible, I'd only like to calculate velocity using data from the final milliseconds of the drag gesture.

langio
  • 81
  • 1
  • 4
  • This is unrelated. I am using DragGesture, which is a part of SwiftUI. I am not using UIPanGestureRecognizer. – langio Jul 26 '19 at 21:47
  • You might want to check out [this answer](https://stackoverflow.com/a/73426600/670119). – Lukáš Kubánek Aug 26 '22 at 16:34
  • Simply use **CACurrentMediaTime** and one line of code, to figure out the value you want yourself. "velocity" is a tricky concept in UX. do you mean "literally that frame", average of last ? frames, average of last ? ms, or what? It's entirely up to you, and you have to just do the division yourself. use **CACurrentMediaTime** for the time since last . – Fattie May 09 '23 at 11:09

6 Answers6

14

You can compute the implied velocity without keeping intermediate state simply by using the predictedEndLocation already supplied by the DragGesture.Value:

DragGesture()
.onChanged { value in 
    // Do something
}
.onEnded { value in
    let velocity = CGSize(
        width:  value.predictedEndLocation.x - value.location.x,
        height: value.predictedEndLocation.y - value.location.y
    )

    // Example

    if velocity.height > 500.0 {
        // Moving down fast
    }
}
dbart
  • 5,468
  • 2
  • 23
  • 19
10

I've been able to get a pretty decent drag velocity value by storing the last drag position as a state and then using that to derive a speed value when the onEnded gets called.

struct MyComponent: View {

    @State var lastDragPosition: DragGesture.Value?

    var body: some View {
        VStack{
            SomeOtherView()
        }.gesture(
            DragGesture().onChanged { value in
                self.lastDragPosition = value
            }
            .onEnded { value in
                let timeDiff = value.time.timeIntervalSince(self.lastDragPosition!.time)
                let speed:CGFloat = CGFloat(value.translation.height - self.lastDragPosition!.translation.height) / CGFloat(timeDiff)

                if(speed > 500) {
                    //Do Something
                }
            }
        )
    }
}
Tom Millard
  • 493
  • 6
  • 19
2

Finally, I got velocity like this. (I wonder why I can't access the velocity of DragGesture.value when print value)

struct ContentView: View {

    @State private var previousDragValue: DragGesture.Value?

    func calcDragVelocity(previousValue: DragGesture.Value, currentValue: DragGesture.Value) -> (Double, Double)? {
        let timeInterval = currentValue.time.timeIntervalSince(previousValue.time)

        let diffXInTimeInterval = Double(currentValue.translation.width - previousValue.translation.width)
        let diffYInTimeInterval = Double(currentValue.translation.height - previousValue.translation.height)

        let velocityX = diffXInTimeInterval / timeInterval
        let velocityY = diffYInTimeInterval / timeInterval
        return (velocityX, velocityY)
    }

    var body: some View {
        return
            VStack {
                …
            }
            .gesture(DragGesture().onChanged {value in
                if let previousValue = self.previousDragValue {
                    // calc velocity using currentValue and previousValue
                    print(self.calcDragVelocity(previousValue: previousValue, currentValue: value))
                }
                // save previous value
                self.previousDragValue = value
            }
    }
}
kazuwombat
  • 1,515
  • 1
  • 16
  • 19
2

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
        
      )

  }
}
Muukii
  • 91
  • 6
0

little late to this party but for anyone who is still looking for an answer.

struct ContentView: View {

    @State var startTime: DragGesture.Value?

    var body: some View {
        MyView()
         .gesture(
            DragGesture()
            .onChanged { value in
                if startTime == nil {
                    startTime = value
                }
             }
            .onEnded { value in
                let distance: CGFloat = value.translation.height
                let time: CGFloat = value.time.timeIntervalSince(startTime!.time)
                
                let velocity: CGFloat = (distance / time) / 1000
                startTime = nil
                
                print(velocity)

            }
        )
    }
}

FE_Tech
  • 1,534
  • 13
  • 25
-3

You honestly don't even need the velocity. Just use DragGesture.Value's predictedEndTranslation

Thasian
  • 57
  • 1
  • 1
  • 7
  • A predictedEndTranslation of a slow drag over a long distance will return the same large values as a fast but short distance and is therefore not useful for providing the drag velocity. – Teo Sartori Nov 21 '20 at 12:33