|
2 | 2 | // VelocityTrackingAnimation.swift |
3 | 3 | // OpenSwiftUICore |
4 | 4 | // |
5 | | -// Audited for iOS 18.0 |
6 | | -// Status: WIP |
| 5 | +// Audited for 6.5.4 |
| 6 | +// Status: Complete |
7 | 7 | // ID: FD9125BC1E04E33D1D7BE4A31225AA98 (SwiftUICore) |
8 | 8 |
|
| 9 | +import Foundation |
| 10 | + |
9 | 11 | // MARK: - TracksVelocityKey |
10 | 12 |
|
11 | 13 | private struct TracksVelocityKey: TransactionKey { |
12 | 14 | static var defaultValue: Bool { false } |
13 | 15 | } |
14 | 16 |
|
| 17 | +@available(OpenSwiftUI_v5_0, *) |
15 | 18 | extension Transaction { |
16 | 19 | /// Whether this transaction will track the velocity of any animatable |
17 | 20 | /// properties that change. |
@@ -50,6 +53,68 @@ extension Transaction { |
50 | 53 | } |
51 | 54 |
|
52 | 55 | extension Animation { |
53 | | - // FIXME: VelocityTrackingAnimation |
54 | | - static let velocityTracking: Animation = .default |
| 56 | + static let velocityTracking: Animation = Animation(VelocityTrackingAnimation()) |
| 57 | +} |
| 58 | + |
| 59 | +private struct VelocityTrackingAnimation: CustomAnimation { |
| 60 | + nonisolated func animate<V>( |
| 61 | + value: V, |
| 62 | + time: TimeInterval, |
| 63 | + context: inout AnimationContext<V> |
| 64 | + ) -> V? where V : VectorArithmetic { |
| 65 | + var sampler = context.velocityState.sampler |
| 66 | + if sampler.isEmpty { // FIXME: Verify this logic |
| 67 | + sampler.addSample(value, time: .init(seconds: time)) |
| 68 | + context.velocityState = .init(sampler: sampler) |
| 69 | + } |
| 70 | + let newTime = (sampler.lastTime?.seconds ?? .zero) + 2.0 |
| 71 | + let velocity = velocity( |
| 72 | + value: value, |
| 73 | + time: time, |
| 74 | + context: context |
| 75 | + ) |
| 76 | + if let velocity, velocity == .zero { |
| 77 | + return nil |
| 78 | + } |
| 79 | + guard newTime > time else { |
| 80 | + return nil |
| 81 | + } |
| 82 | + return value |
| 83 | + } |
| 84 | + |
| 85 | + |
| 86 | + nonisolated func velocity<V>( |
| 87 | + value: V, |
| 88 | + time: TimeInterval, |
| 89 | + context: AnimationContext<V> |
| 90 | + ) -> V? where V : VectorArithmetic { |
| 91 | + let timeDiff = time - (context.velocityState.sampler.lastTime?.seconds ?? .zero) |
| 92 | + let scale = pow(0.998, timeDiff * 1000) |
| 93 | + return context.velocityState.sampler.velocity.scaled(by: scale).valuePerSecond |
| 94 | + } |
| 95 | + |
| 96 | + nonisolated func shouldMerge<V>( |
| 97 | + previous: Animation, |
| 98 | + value: V, |
| 99 | + time: TimeInterval, |
| 100 | + context: inout AnimationContext<V> |
| 101 | + ) -> Bool where V: VectorArithmetic { |
| 102 | + context.velocityState.sampler.addSample(value, time: .init(seconds: time)) |
| 103 | + return true |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +extension AnimationContext { |
| 108 | + fileprivate var velocityState: VelocityState<Value> { |
| 109 | + get { state[VelocityState<Value>.self] } |
| 110 | + set { state[VelocityState<Value>.self] = newValue } |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +private struct VelocityState<Value>: AnimationStateKey where Value: VectorArithmetic { |
| 115 | + static var defaultValue: VelocityState { |
| 116 | + VelocityState(sampler: .init()) |
| 117 | + } |
| 118 | + |
| 119 | + var sampler: VelocitySampler<Value> |
55 | 120 | } |
0 commit comments