Skip to main content
This guide will walk you through creating your first Wave animation. You’ll learn how to animate a view using spring physics and understand the core concepts.
Make sure you’ve installed Wave before continuing.

Your first animation

Let’s create a simple animation that moves a view to the center of the screen with a bouncy spring effect.
1

Import Wave

Start by importing Wave in your view controller:
import UIKit
import Wave
2

Create a view to animate

Add a colored box to your view:
class ViewController: UIViewController {
    let box = UIView(frame: CGRect(x: 50, y: 100, width: 80, height: 80))
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        box.backgroundColor = .systemBlue
        box.layer.cornerRadius = 16
        view.addSubview(box)
    }
}
3

Animate with Wave

Add a button or call this in viewDidAppear to trigger the animation:
func animateBox() {
    // Create a spring with some bounce
    let spring = Spring(dampingRatio: 0.6, response: 0.8)
    
    Wave.animate(withSpring: spring) {
        // Animate to center with scale and rotation
        self.box.animator.center = self.view.center
        self.box.animator.backgroundColor = .systemPink
        self.box.animator.scale = CGPoint(x: 1.5, y: 1.5)
    }
}
Notice we use box.animator.center instead of box.center. This is required for Wave to track and animate the property.
That’s it! Run your app and you’ll see a smooth, spring-based animation with natural physics.

Understanding springs

The Spring type controls how your animation moves. It has two key parameters:

Damping ratio

Controls the “bounciness” of the animation:
  • 1.0: Critically damped - smoothly reaches the target with no oscillation
  • < 1.0: Underdamped - overshoots and bounces before settling
  • > 1.0: Overdamped - slowly approaches the target without overshooting

Response

Controls how quickly the animation reaches its target (measured in seconds):
  • Lower values (0.2 - 0.4): Fast, snappy animations
  • Medium values (0.5 - 0.8): Balanced, natural feeling
  • Higher values (1.0+): Slow, deliberate animations
// Quick and bouncy - great for interactive gestures
let spring = Spring(dampingRatio: 0.6, response: 0.3)

Default springs

Wave provides pre-configured springs for common use cases:
// For interactive animations (dragging, gestures)
let interactive = Spring.defaultInteractive  // dampingRatio: 0.8, response: 0.2

// For non-interactive animations (transitions, state changes)
let animated = Spring.defaultAnimated  // dampingRatio: 1.0, response: 0.82

Animatable properties

Wave’s block-based API supports animating these UIView and CALayer properties:

Layout

frame, bounds, center, origin

Appearance

alpha, backgroundColor

Transform

scale, translation

Layer

cornerRadius, borderColor, borderWidth, shadowColor, shadowOpacity, shadowOffset, shadowRadius

Adding gesture velocity

One of Wave’s most powerful features is the ability to inject gesture velocity into animations. This creates incredibly fluid, responsive interactions.
class ViewController: UIViewController {
    let draggableView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        draggableView.backgroundColor = .systemOrange
        draggableView.layer.cornerRadius = 20
        view.addSubview(draggableView)
        
        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
        draggableView.addGestureRecognizer(pan)
    }
    
    var initialCenter: CGPoint = .zero
    
    @objc func handlePan(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)
        let velocity = gesture.velocity(in: view)
        
        switch gesture.state {
        case .began:
            initialCenter = draggableView.center
            
        case .changed:
            // Use a tight spring for tracking the gesture
            let interactiveSpring = Spring(dampingRatio: 0.8, response: 0.2)
            
            Wave.animate(withSpring: interactiveSpring) {
                self.draggableView.animator.center = CGPoint(
                    x: self.initialCenter.x + translation.x,
                    y: self.initialCenter.y + translation.y
                )
            }
            
        case .ended:
            // Use a looser spring for the final animation
            let animatedSpring = Spring(dampingRatio: 0.68, response: 0.8)
            
            // Inject the gesture velocity for natural momentum
            Wave.animate(withSpring: animatedSpring, gestureVelocity: velocity) {
                self.draggableView.animator.center = self.view.center
            }
            
        default:
            break
        }
    }
}
Passing gestureVelocity to the animation preserves the momentum from the gesture, making the animation feel continuous and natural.

Completion callbacks

Wave supports completion callbacks that tell you when an animation finishes or gets retargeted:
Wave.animate(withSpring: Spring.defaultAnimated) {
    box.animator.alpha = 0.0
} completion: { finished, retargeted in
    if finished {
        print("Animation completed successfully")
        // Remove the view, trigger another animation, etc.
    }
    
    if retargeted {
        print("Animation was interrupted and retargeted")
        // The target value changed mid-animation
    }
}
  • finished: true if the animation completed normally
  • retargeted: true if the target value changed while animating

Understanding retargeting

Retargeting is what makes Wave special. You can change an animation’s target value at any time, and Wave will smoothly redirect:
// Start animating to the right
Wave.animate(withSpring: Spring.defaultAnimated) {
    box.animator.center.x = 300
}

// Before it finishes, change direction
// The animation will smoothly arc to the new target
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
    Wave.animate(withSpring: Spring.defaultAnimated) {
        self.box.animator.center.x = 100
    }
}
The second animation doesn’t abruptly reset the motion. Instead, Wave preserves the velocity and creates a smooth arc to the new destination.
Retargeting is especially powerful for gesture-driven UIs where users frequently change direction mid-interaction.

Complete example

Here’s a complete, working example that demonstrates multiple concepts:
ViewController.swift
import UIKit
import Wave

class ViewController: UIViewController {
    
    let box = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
    let resetButton = UIButton(type: .system)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        // Configure the animated box
        box.backgroundColor = .systemBlue
        box.layer.cornerRadius = 16
        box.center = CGPoint(x: 100, y: 200)
        view.addSubview(box)
        
        // Add tap gesture to the box
        let tap = UITapGestureRecognizer(target: self, action: #selector(boxTapped))
        box.addGestureRecognizer(tap)
        
        // Configure reset button
        resetButton.setTitle("Reset", for: .normal)
        resetButton.frame = CGRect(x: 0, y: 0, width: 100, height: 44)
        resetButton.center = CGPoint(x: view.center.x, y: view.bounds.height - 100)
        resetButton.addTarget(self, action: #selector(reset), for: .touchUpInside)
        view.addSubview(resetButton)
    }
    
    @objc func boxTapped() {
        let spring = Spring(dampingRatio: 0.65, response: 0.7)
        
        Wave.animate(withSpring: spring) {
            // Animate multiple properties simultaneously
            self.box.animator.center = self.view.center
            self.box.animator.backgroundColor = .systemPink
            self.box.animator.scale = CGPoint(x: 1.5, y: 1.5)
            self.box.animator.cornerRadius = 40
        } completion: { finished, retargeted in
            if finished {
                print("Box animation completed!")
            }
        }
    }
    
    @objc func reset() {
        let spring = Spring(dampingRatio: 0.8, response: 0.5)
        
        Wave.animate(withSpring: spring) {
            self.box.animator.center = CGPoint(x: 100, y: 200)
            self.box.animator.backgroundColor = .systemBlue
            self.box.animator.scale = CGPoint(x: 1.0, y: 1.0)
            self.box.animator.cornerRadius = 16
        }
    }
}

Next steps

Now that you understand the basics, explore these topics:

Block-based animation

Deep dive into the block-based API and all animatable properties

Property-based animation

Learn to use SpringAnimator for custom animations

Spring physics

Master spring configuration and timing

Examples

See real-world examples and common patterns

Build docs developers (and LLMs) love