Open-source project
jtrivedi/Wave avatar
jtrivedi/Wave

Wave: Retargetable Spring Animations for UIKit, SwiftUI and AppKit

Wave is a spring-based animation engine for iOS and macOS that makes it easy to create fluid, interruptible animations that feel great.

2,397 stars72 forksSwiftMIT

At a glance

What is it?
Wave is a dependency-free Swift animation engine built around re-targetable springs. It fits gesture-driven interfaces that need to redirect mid-flight, and it stops short of being a general animation framework.
Who is it for?
Wave is worth adopting if your interface is gesture-driven and you keep changing an animation's destination while it is still running: the retargeting model and the gestureVelocity parameter exist for exactly that case, and the MIT licence keeps the integration cost low.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What Wave solves, and who ends up using it

Most animation APIs assume a destination is final. You start an animation toward a point, and if the finger moves or the layout changes, you cancel it and start another one. The visible result is a velocity discontinuity: the view stops, then restarts from zero speed in a new direction. Wave is built around the opposite assumption. Its core feature, in the README's words, is that all animations are re-targetable, meaning the destination value can change in-flight and the animation redirects to the new value. The README frames retargeting as preserving an animation's velocity even as its target changes, and points to a WWDC 2018 session on the subject.

The audience is narrow but real. This is a library for people writing interactive iOS, iPadOS or macOS interfaces: drag-to-dismiss sheets, picture-in-picture windows, cards that follow a finger and settle when released, or a progress value driven by a spring. The README's own demonstration is the iOS Picture-in-Picture feature, comparing a standard UIKit implementation on the left with a Wave implementation on the right, and describing the UIKit version as stiff and jerky by comparison. If your animations are triggered once and run to completion, Wave's central mechanism is unused weight.

How retargeting works in the API

Wave exposes two layers. The block-based API resembles UIView.animateWithDuration and handles a fixed set of UIView and CALayer properties. Under the hood, the README states, Wave creates, manages and executes the required spring animations for you. The property-based API gives you a SpringAnimator that produces intermediate values, which you drive yourself.

The mechanism that makes retargeting possible is that the animator holds a current value, a target value and a velocity as separate pieces of state. In the property-based example, you assign positionAnimator.value from the view's presentation value, positionAnimator.target to the destination, and positionAnimator.velocity to the gesture's lift-off velocity. Because the target is just a field, changing it mid-flight does not restart the animation; the spring continues from its current value and velocity toward the new target. That is why the README can say you may retarget the center property at any time.

One consequence is easy to miss. The completion block signature is (finished, retargeted). If an animation completes fully, finished is true. If its target changed in-flight, finished is false and retargeted is true. Any code that treats finished == false as a cancellation, for example a cleanup path that removes a view, will misbehave under retargeting.

Installing Wave and animating a view to a new target

Wave has no external dependencies and ships as a Swift package. The README gives two installation routes: add the package to your app's Package.swift, or use File -> Add Packages in Xcode. The package line is:

swift
.package(url: "https://github.com/jtrivedi/Wave")

If you clone the repository, the README notes you can run the sample app, which contains interactive demos. The repository layout shows a Sample App directory alongside Sources, Tests and a Wave-Sample Xcode project that the README says you can open and run.

For a first real use, the block-based API is the shortest path. The README's picture-in-picture example creates a Spring with a dampingRatio and a response (the latter affects duration), reads the gesture's lift-off velocity, and animates properties through the view's animator property rather than the view itself:

swift
if panGestureRecognizer.state == .ended {
    let animatedSpring = Spring(dampingRatio: 0.68, response: 0.80)
    let gestureVelocity = panGestureRecognizer.velocity(in: view)

    Wave.animate(withSpring: animatedSpring, gestureVelocity: gestureVelocity) {
        pipView.animator.center = pipViewDestination
        pipView.animator.scale = CGPoint(x: 1.1, y: 1.1)
    }
}

After running this, the view should settle at the destination with the gesture's momentum carried into the spring. If you want the intermediate values, for instance to draw a path from the starting center to the destination, the property-based API gives you a valueChanged callback:

swift
let positionAnimator = SpringAnimator<CGPoint>(spring: animatedSpring)
positionAnimator.value = pipView.center
positionAnimator.target = pipViewDestination
positionAnimator.velocity = gestureVelocity

positionAnimator.valueChanged = { [weak self] location in
    self?.drawPathPoint(at: location)
}

positionAnimator.start()

One platform detail belongs in your setup rather than your animation code: to enable high frame-rate animations on ProMotion devices, the README says to set the Info.plist key CADisableMinimumFrameDuration to true. Without it, animations are capped at 60 fps.

The property list is the real boundary

The block-based API supports a specific set of properties: frame, bounds, center, origin, alpha, backgroundColor, cornerRadius, scale, translation, shadowColor, shadowRadius, shadowOffset, shadowOpacity, borderColor and borderWidth. Rotation is listed as upcoming, not supported. That list is the practical limit of the convenient API, and it is worth reading before designing an interaction around it. A rotation-driven animation has to go through SpringAnimator and be applied by hand, which means you own the mapping from spring value to transform.

There is a second boundary that the README does not address at all. It never documents cancellation, rollback, or what happens if you release the view and the animator while an animation is in flight. It also does not describe thread or actor requirements, or how the animator behaves when a view is removed from its window mid-animation. Those are ordinary questions for production code, and the documentation is silent on them, so the sample app in the repository is the place to look.

Finally, retargeting is not free conceptually. Every completion handler in your codebase has to decide what retargeted means for its own logic. If your team's habit is to treat any non-finished completion as a failure path, Wave will surface that habit quickly.

Where Wave sits next to SwiftUI's animation modifiers

The closest everyday alternative is SwiftUI's built-in animation system, withAnimation and the animation modifiers. The difference is in what carries state between interruptions. SwiftUI's model is declarative: you change state, and the framework interpolates from the current presentation value to the new one. It handles interruption, but the API does not hand you a spring object you can retarget explicitly, and it does not take a gesture's lift-off velocity as a first-class input the way Wave's animate(withSpring:gestureVelocity:) does.

Wave's model is imperative and value-oriented. You hold a Spring with a dampingRatio and a response, and in the property-based API you hold an animator whose value, target and velocity are yours to set. That is more code for a simple fade, and less friction for a card that must arc when a finger releases. The README also notes Wave drops into existing UIKit, SwiftUI or AppKit projects, so the choice is not exclusive to one UI framework. If your interface is mostly state-driven transitions with no gesture velocity to preserve, SwiftUI's modifiers cover the ground and Wave adds a dependency for behaviour you will not exercise.

Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-03-25. Releases are sparse: 0.3.1 in January 2023, 0.3.2 in February 2023, and 0.3.3 in February 2025. The documentation site at jtrivedi.github.io/Wave is the reference for full API and usage, and the repository contains a generate_docs.sh script and a docs directory, so the site is generated from the source tree rather than maintained by hand.

Wave is MIT licensed. For most teams that means the licence itself is not the deciding factor; the practical implication is that you can vendor the package or fork it if upstream slows down, provided you keep the licence text. This is not legal advice, and if your organisation has a policy on third-party dependencies, that policy governs.

Upgrade cost is low in principle because the package has no external dependencies, so a version bump does not drag a graph of transitive packages with it. The real cost is behavioural: a spring's feel is defined by dampingRatio and response, and any change to those defaults or to the retargeting maths changes how existing interactions move. Pin the version and re-check your gesture-driven screens when you move it.

Editorial conclusion

Wave is worth adopting if your interface is gesture-driven and you keep changing an animation's destination while it is still running: the retargeting model and the gestureVelocity parameter exist for exactly that case, and the MIT licence keeps the integration cost low. It is the wrong tool if you need to animate properties outside the documented list, because the block-based API covers frame, bounds, center, origin, alpha, backgroundColor, cornerRadius, scale, translation, shadow properties and border properties, and the README still lists rotation as upcoming. Before you commit, verify three things in your own project: that your target properties are on that list, that CADisableMinimumFrameDuration is set to true in Info.plist if you want ProMotion frame rates, and that your completion handlers treat the retargeted flag as a normal outcome rather than an error. The last one matters most, because a retargeted animation reports finished as false.

Frequently asked questions

How do I install Wave in an iOS or macOS project?

Wave is a Swift package with no external dependencies. The README says to add it to your app's Package.swift file or to select File -> Add Packages in Xcode, using the repository URL https://github.com/jtrivedi/Wave.

What does retargeting mean in Wave?

The README describes retargeting as preserving an animation's velocity even as its target changes, and states that all Wave animations are re-targetable, so a destination can be changed in-flight and the animation redirects to the new value. Wave does this automatically.

Which properties can Wave animate with the block-based API?

The README lists frame, bounds, center, origin, alpha, backgroundColor, cornerRadius, scale, translation, shadowColor, shadowRadius, shadowOffset, shadowOpacity, borderColor and borderWidth. Rotation is listed as an upcoming property, so it is not supported by the block-based API yet.

Why is my Wave animation capped at 60 fps on a ProMotion device?

The README states that high frame-rate animations on ProMotion devices require the Info.plist key CADisableMinimumFrameDuration to be set to true. Without that entry, animations are capped at 60 fps.

How do I know whether a Wave animation finished or was retargeted?

Both the block-based and property-based APIs support completion blocks with a finished and a retargeted flag. The README says finished is true if the animation completes fully, and false with retargeted set to true if the target changed in-flight.

Official sources

  1. jtrivedi/Wave on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jtrivedi-wave.svg)](https://hysenlabs.com/projects/jtrivedi-wave)