Skip to content

Latest commit

 

History

History
474 lines (355 loc) · 11.2 KB

File metadata and controls

474 lines (355 loc) · 11.2 KB

VDAnimation Motion Guide

This comprehensive guide demonstrates the capabilities of the VDAnimation library, explaining each motion type, their usage patterns, and examples.

Table of Contents

Basic Concepts

  • 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

Getting Started

Basic SwiftUI Integration

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)
            }
    }
}

Motion Types

To

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 })

Parameters

  • targetValue: The final value to animate towards
  • values: Multiple values to animate through in sequence
  • lerp: A custom function that interpolates between values

Sequential

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)
}

Parallel

The Parallel motion runs multiple animations simultaneously. It provides three main approaches for animating different aspects of a value.

1. Animating by KeyPaths

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) }

2. Animating by Collection Indices

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))
}

3. Animating by Dictionary Keys

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)
    }
}

From

The From motion sets a specific starting value for an animation.

From(startValue) {
    To(1.0).duration(0.3)
}

Wait

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)

Parameters

  • 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.

Instant

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)
}

SideEffect

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.

Repeat

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)
    }
}

AutoReverse

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)
}

Sync

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)
}

Steps

The Steps motion animates through discrete values with jumps rather than smooth transitions.

Steps(0.0, 0.3, 0.7, 1.0)

TransformTo

The TransformTo motion animates using a transformation function applied to the initial value.

TransformTo { value in value * 2 }

Lerp

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)
}

Common Modifiers

All motion types support the following modifiers:

Duration

motion.duration(1.0)  // In seconds
motion.duration(.absolute(1.0))  // Absolute time
motion.duration(.relative(0.5))  // Relative to parent motion

Note: 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.

Delay

motion.delay(0.2)  // Delay in seconds
motion.delay(.relative(0.1))  // Relative delay

DelayAfter

motion.delayAfter(0.2)  // Delay after the motion ends  
motion.delayAfter(.relative(0.1))  // Relative delay after the motion ends

Curve

motion.curve(.linear)
motion.curve(.easeInOut)
motion.curve(.spring(damping: 0.7, velocity: 0.3))

Animation Curves

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:

Basic Curves

  • .linear - Constant speed
  • .easeIn - Accelerating
  • .easeOut - Decelerating
  • .easeInOut - Accelerating then decelerating

Advanced Curves

  • .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

Special Curves

  • .step(threshold) - Step function that jumps at threshold
  • .interval(range) - Maps to specific range
  • .spring(damping, velocity) - Spring physics

Integration with SwiftUI

MotionState

@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)

withMotion modifier and WithMotion View

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)
}

Animation Control

// 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 directly

Working with UIKit

VDAnimation 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()
    }
}

Advanced Usage

Custom Tweenables

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 Tweenable conformance
  • Codable types have default Tweenable conformance

Note: Tweenable is not required for motions to work, but it is required for To motion.