Wave is a spring-based animation engine for iOS and iPadOS. It makes it easy to create fluid, interactive, and interruptible animations that feel great.
Wave has no external dependencies, and can be easily dropped into existing UIKit-based projects and apps.
The core feature of Wave is that all animations are re-targetable, meaning that you can change an animation’s destination value in-flight, and the animation will gracefully redirect to that new value.
Consider these demos of the iOS Picture-in-Picture feature. The screen on the left is created with standard UIKit animations, and the one on the right is created with Wave.
Though both are “interruptible”, the Wave-based implementation handles the interruption much better, and fluidly arcs to its new destination. The UIKit animation feels stiff and jerky in comparison.
At its core, retargeting is the process of preserving an animation’s velocity even as its target changes, which Wave does automatically.
Add Wave to your app's Package.swift
file, or selecting File -> Add Packages
in Xcode:
.package(url: "https://github.com/jtrivedi/Wave")
If you clone the repo, you can run the sample app, which contains a few interactive demos to understand what Wave provides.
There’s a full Wave documentation site available for full API and usage documentation.
There are two ways you can interact with Wave, depending on your needs: the block-based and property-based animations:
The easiest way to get started is by using Wave’s block-based APIs that resemble the UIView.animateWithDuration()
APIs.
This API lets you animate several common UIView and CALayer properties, like frame
, center
, scale
, backgroundColor
, and more.
For these supported properties, Wave will create, manage, and execute the required spring animations under-the-hood.
For example, animating the above PiP view to its final destination is extremely simple:
if panGestureRecognizer.state == .ended {
// Create a spring with some bounciness. `response` affects the animation's duration.
let animatedSpring = Spring(dampingRatio: 0.68, response: 0.80)
// Get the gesture's lift-off velocity
let gestureVelocity = panGestureRecognizer.velocity(in: view)
Wave.animate(withSpring: animatedSpring, gestureVelocity: touchVelocity) {
// Update the `center` and `scale` properties of the view's _animator_, not the view itself.
pipView.animator.center = pipViewDestination
pipView.animator.scale = CGPoint(x: 1.1, y: 1.1)
}
}
Note that at any time, you can retarget the view’s center
property to somewhere else, and it’ll gracefully animate.
The block-based API currently supports animating the following properties. For other properties, you can use the property-based animation API below.
frame
bounds
center
origin
alpha
backgroundColor
cornerRadius
scale
translation
Upcoming properties:
rotation
shadow color/radius/offset/opacity
While the block-based API is often most convenient, you may want to animate something that the block-based API doesn’t yet support (e.x. rotation). Or, you may want the flexibility of getting the intermediate spring values and driving an animation yourself (e.x. a progress value).
For example, to draw the orange path of the PiP demo, we need to know the value of every CGPoint
from the view’s initial center, to its destination center:
// When the gesture ends, create a `CGPoint` animation from the PiP view's initial center, to its target.
// The `valueChanged` callback provides the intermediate locations of the callback, allowing us to draw the path.
let positionAnimator = Animation<CGPoint>(spring: animatedSpring)
positionAnimator.value = pipView.center // The presentation value
positionAnimator.target = pipView.animator.center // The target value
positionAnimator.velocity = touchVelocity
positionAnimator.valueChanged = { [weak self] location in
self?.drawPathPoint(at: location)
}
positionAnimator.start()
Both the block-based and property-based APIs support completion blocks. If an animation completes fully, the completion block’s finished
flag will be true.
However, if an animation’s target was changed in-flight (“retargeted”), finished
will be false, while retargeted
will be true.
Wave.animate(withSpring: Spring.defaultAnimated) {
myView.animator.backgroundColor = .systemBlue
} completion: { finished, retargeted in
print(finished, retargeted)
}
Exploring the provided sample app is a great way to get started with Wave.
Simply open the Wave-Sample
Xcode project and hit “Run”. The full source code for the Picture-in-Picture demo is available there, too!
Special thanks to Ben Oztalay for helping architect the underlying physics of Wave!