This comprehensive guide demonstrates the capabilities of the VDAnimation library, explaining each motion type, their usage patterns, and examples.
- Basic Concepts
- Getting Started
- Motion Types
- Common Modifiers
- Animation Curves
- Integration with SwiftUI
- Working with UIKit
- Advanced Usage
- Motion: A type that describes an animation from one value to another
- MotionState: A property wrapper that holds an initial value and its animation controller
- WithMotion: A SwiftUI view that applies motion animations to its content
- Tweenable: A protocol that enables types to be animated by defining interpolation between values
struct SimpleAnimation: View {
@MotionState private var scale = 1.0
var body: some View {
Circle()
.fill(Color.blue)
.withMotion(_scale) { view, value in
view.scaleEffect(value)
} motion: {
To(2.0).duration(1.0).curve(.easeInOut).autoreverse()
}
.frame(width: 100, height: 100)
.onAppear {
$scale.play(repeat: true)
}
}
}The To motion animates a value towards a target value or multiple target values.
To(targetValue)
To(value1, value2, value3)
To(value1, value2, value3, lerp: { $0 * $1 })
To(arrayOfValues)
To(arrayOfValues, lerp: { $0 * $1 })targetValue: The final value to animate towardsvalues: Multiple values to animate through in sequencelerp: A custom function that interpolates between values
The Sequential motion runs multiple animations one after another in sequence.
Sequential {
To(1.0).duration(0.3)
Wait(0.5)
To(0.0).duration(0.3)
}The Parallel motion runs multiple animations simultaneously. It provides three main approaches for animating different aspects of a value.
For struct properties using keypath syntax.
// Method 1: Using KeyPath builder
Parallel<CGSize>() // Generic type can be omitted in most cases.
.at(\.width) { To(100.0).curve(.easeOut) }
.at(\.height) { To(200.0) }
// Method 2: Using dynamic member lookup and MotionBuilder
Parallel<CGSize>()
.width {
To(100.0).duration(0.5).curve(.easeOut)
}
// Method 3: Using dynamic member lookup and callAsFunction shorthand
Parallel<CGSize>()
.width(100, 200, 300) // Shorthand for .at(\.width) { To(100, 200, 300) }For mutable collections elements using index-based access.
Parallel<[Double]> { index in
// index is the position in the array
To(10.0).delay(.relative(Double(index) * 0.1))
}For dictionary elements using key-based access.
Parallel<[String: CGFloat]> { key in
if key == "opacity" {
To(1.0).duration(0.3)
} else if key == "scale" {
To(1.2).duration(0.5).curve(.easeOut)
} else {
0 // Motion builder allows just put the value to animate. Same as To(0)
}
}The From motion sets a specific starting value for an animation.
From(startValue) {
To(1.0).duration(0.3)
}The Wait motion pauses the animation for a specified duration or all available time.
Wait(1.0)
Wait(.relative(0.5))
Sequential {
To(1.0).duration(0.3)
Wait() // Pause for 0.4 seconds
To(0.0).duration(0.3)
}
.duration(1.0)duration: Duration to wait, can be absolute or relative. Optional parameter. When not provided, the duration will be the remaining time of the parent motion.
The Instant motion immediately changes to a specified value without animation.
Sequential {
To(0.5).duration(0.3)
Instant(1.0) // Jump immediately to 1.0
Wait(0.2)
To(0.0).duration(0.3)
}The SideEffect motion executes a closure at a specific point in the animation.
SideEffect {
FeedbackManager.triggerHapticFeedback()
}
SideEffect { value in
// Action that receives the current value
}
Sequential {
To(0.5).duration(0.3)
SideEffect {
hapticFeedback()
}
To(1.0).duration(0.3)
}Note: SideEffect is executed asynchronously.
The Repeat motion repeats an animation a specified number of times.
// Method 1: Using modifier
To(1.0).duration(0.5).repeat(3)
// Method 2: Using wrapper
Repeat(3) {
Sequential {
To(1.2).duration(0.15)
To(0.9).duration(0.15)
}
}The AutoReverse motion plays an animation forward and then in reverse (ping-pong effect).
// Method 1: Using modifier
To(1.2).curve(.easeOut).autoreverse()
// Method 2: Using wrapper
AutoReverse {
To(1.2).duration(0.5).curve(.easeOut)
}The Sync motion synchronizes animation with system time for continuous animations that should remain smooth even if the UI temporarily freezes.
// Method 1: Using modifier
To(CGFloat.pi * 2).autoreverse().duration(1.0).sync()
// Method 2: Using wrapper
Sync {
To(CGFloat.pi * 2).autoreverse().duration(1.0)
}The Steps motion animates through discrete values with jumps rather than smooth transitions.
Steps(0.0, 0.3, 0.7, 1.0)The TransformTo motion animates using a transformation function applied to the initial value.
TransformTo { value in value * 2 }The Lerp motion interpolates using a custom function.
Lerp { initial, t in
// Custom interpolation logic
return initial * t
}
Lerp { t in
// Custom interpolation logic
return sin(t * .pi)
}All motion types support the following modifiers:
motion.duration(1.0) // In seconds
motion.duration(.absolute(1.0)) // Absolute time
motion.duration(.relative(0.5)) // Relative to parent motionNote: Duration doesn't guarantee the exact duration of the animation, because the animation may prefer it's own duration. For example, Instant motion will always be 0 duration.
motion.delay(0.2) // Delay in seconds
motion.delay(.relative(0.1)) // Relative delaymotion.delayAfter(0.2) // Delay after the motion ends
motion.delayAfter(.relative(0.1)) // Relative delay after the motion endsmotion.curve(.linear)
motion.curve(.easeInOut)
motion.curve(.spring(damping: 0.7, velocity: 0.3))In VDAnimation, curve is a function that takes a progress value and returns a new progress value. So you can implement literally any curve you want.
VDAnimation provides a rich set of built-in animation curves:
.linear- Constant speed.easeIn- Accelerating.easeOut- Decelerating.easeInOut- Accelerating then decelerating
.cubicEaseIn,.cubicEaseOut,.cubicEaseInOut- Cubic easings with more pronounced acceleration.elasticEaseIn,.elasticEaseOut- Elastic effect (overshooting with oscillation).bounceEaseIn,.bounceEaseOut- Bouncing effect.sineEaseIn,.sineEaseOut,.sineEaseInOut- Sine-based easings.backEaseIn,.backEaseOut- Overshooting animation that backs up before/after animating
.step(threshold)- Step function that jumps at threshold.interval(range)- Maps to specific range.spring(damping, velocity)- Spring physics
@MotionState private var opacity = 0.0
@MotionState private var position = CGPoint.zero
@MotionState private var colors: [Color] = [.red, .green, .blue]
// For types that require custom initialization
@MotionState private var complexState = MyTweenable(initialValue: 0)Circle()
.withMotion(_state) { view, value in
view.opacity(value)
} motion: {
To(1.0).duration(0.5)
}
// or
WithMotion(_state) { value in
// View using animated value
Circle()
.opacity(value)
} motion: {
// Motion definition
To(1.0).duration(0.5)
}// Start animation
$state.play() // Play once
$state.play(repeat: true) // Play repeatedly
// Control
$state.pause() // Pause animation
$state.reverse() // Reverse direction
$state.toggle() // Toggle between play/pause
// Jump to specific progress
$state.progress = 0.5 // Set progress directlyVDAnimation also works with UIKit through a CADisplayLink wrapper.
final class UIKitExampleView: UIView {
let label = UILabel()
override func didMoveToWindow() {
super.didMoveToWindow()
guard window != nil else { return }
setupUI()
// Start animation
motionDisplayLink(0) { [weak self] value in
self?.updateUI(with: value)
} motion: {
To(1000).duration(2.0).curve(.easeInOut)
}
.play()
}
}You can make your own types animatable by conforming to the Tweenable protocol:
// Method 1: Using macro
@Tweenable
struct MyAnimatableType {
var progress: Double
var color: Color
}
// Method 2: Manually conform to Tweenable
struct MyAnimatableType: Tweenable {
var progress: Double
var color: Color
static func lerp(_ a: MyAnimatableType, _ b: MyAnimatableType, _ t: Double) -> MyAnimatableType {
MyAnimatableType(
progress: .lerp(a.progress, b.progress, t),
color: .lerp(a.color, b.color, t)
)
}
}
// Method 3: Extending existing Codable type
extension MyCodableType: Tweenable {}VDAnimation add Tweenable conformance to most common types:
- All numeric types
- CoreAnimation types: CGPoint, CGSize, CGRect, CGAffineTransform, CATransform3D, CGVector, CGAffineTransformComponents
- Pathes: CGPath, UIBezierPath/NSBezierPath, Path.
- Color types: UIColor/NSColor, CGColor, Color. Colors uses OKLab color space for lerp by default, but it can be configured to RGB or OKLCH.
- EdgeInsets, UIEdgeInsets/NSEdgeInsets, NSDirectionalEdgeInsets
- Collections of Tweenable: Array, Dictionary, ContiguousArray, ArraySlice
- Range, ClosedRange, PartialRangeFrom, PartialRangeUpTo, PartialRangeThrough
- AnimatablePair, EmptyAnimatablePair
- Angle
- SIMD types
- VectorArithmetic types have default
Tweenableconformance - Codable types have default
Tweenableconformance
Note: Tweenable is not required for motions to work, but it is required for To motion.