SkillAgentSearch skills...

Motion

Animation engine for gesturally-driven user interfaces, animations, and interactions on iOS, macOS, and tvOS.

Install / Use

/learn @b3ll/Motion
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Motion-Logo-Dark Motion-Logo-Light

Tests Docs

Motion is an animation engine for gesturally-driven user interfaces, animations, and interactions on iOS, macOS, and tvOS, and is powered by SIMD and written fully in Swift. Motion allows for easily creating physically-modeled, interruptible animations (i.e. springs, decays, etc.) that work hand-in-hand with gesture recognizers to make the most fluid and delightful interactions possible.

Usage

API Documentation is here

Animations

Creating animations in Motion is relatively simple. Simply allocate the animation type that you want with the type that conforms to SIMDRepresentable, configure it, and call start to start it. For each frame the animation executes, its onValueChanged block will be called, and you'll be given the opportunity to assign that newly animated value to something.

By default, lots of types are already supported out of the box, including:

  • Float
  • Double
  • CGFloat
  • CGPoint
  • CGSize
  • CGRect
  • SIMD2<Float>
  • SIMD2<Double>
  • … and many more.

Calling stop will freeze it in place, without the need to query the presentationLayer on CALayer and set values, or worry about fillMode, or worry about anything really.

Whenever it's done, the completion block will be called.

The animations need to be held somewhere as they will stop running if they're deallocated. Also, due to the nature of how they execute blocks, be careful not to introduce retain cycles by not using a weak self or unowned animation inside the animation onValueChanged or completion block.

Here's some examples:

Spring Animation

let springAnimation = SpringAnimation<CGRect>()
springAnimation.configure(response: 0.30, damping: 0.64)
springAnimation.toValue = CGRect(x: 0.0, y: 0.0, width: 320.0, height: 320.0)
springAnimation.velocity = CGRect(x: 0.0, y: 0.0, width: -200.0, height: -200.0)
springAnimation.onValueChanged(disableActions: true) { newValue in
    view.layer.bounds = newValue
}
springAnimation.completion = { [weak self] in
    // all done
    self?.animationDone()
}
springAnimation.start()

Note: Some of you may be wondering if it's a mistake that the stiffness, damping, response, or dampingRatio setters are private, however this is intentional. It's incredibly easy to mixup damping and dampingRatio, and using one over the other will lead to dramatically different results. In addition, you should only be configuring either stiffness and damping or response and dampingRatio as they're both two separate ways of configuring spring constants.

Decay Animation

let decayAnimation = DecayAnimation<CGPoint>()
decayAnimation.velocity = CGPoint(x: 0.0, y: 2000.0)
decayAnimation.onValueChanged { newValue in
    view.bounds.origin = newValue
}
decayAnimation.completion = {
    // all done
}
decayAnimation.start()

Basic Animation

let basicAnimation = BasicAnimation<CGFloat>(easingFunction: .easeInOut)
basicAnimation.fromValue = 100.0
basicAnimation.toValue = 200.0
basicAnimation.duration = 0.4
basicAnimation.onValueChanged { newValue in
    view.bounds.frame.x = newValue
}
basicAnimation.completion = {
    // all done
}
basicAnimation.start()

Note: All of these animations are to run and be interfaced with on the main thread only. There is no support for threading of any kind.

SwiftUI Support

Motion supports SwiftUI out of the box! You can use any Animation subclass to animate @State changes manually.

Checkout the example project's SwiftUI Demo for more info!

Note: Work is planned to support TimelineView. Stay tuned!

Motion vs. Core Animation

Motion is not designed to be a general-purpose replacement for Core Animation. Core Animation animations are run in a special way, in another process, outside of your app and are designed to be smooth even when the main thread is being heavily used. Motion on the other hand is all run in-process (like a game engine), and using it liberally without considering heavy stack traces, will result in poor performance and dropped frames. Motion itself is not slow (in fact it's really fast!), but calling methods to change view / layer properties or change layout at 60 FPS (or more) can be really taxing if not done carefully.

tl;dr: Treat Motion animations as you would a UIScrollView (since scrolling animations behave the same way). If you have too much going on in your UIScrollView it'll lag when it scrolls; the same applies to Motion.

Some key tips:

  • Measure text / layout asychronously, and then commit those changes back to the main thread whenever possible.
  • Layout a view controller fully before presenting (rather than during presenting) using setNeedsDisplay() and layoutIfNeeded().
  • Avoid expensive operations during gestures / handing off from gestures.
  • If you can't optimize things any further, using CAKeyframeAnimationEmittable will help, and that's outlined later in this guide.

Interruptibility

Motion is designed out of the box to make interruptible animations much easier. Interruptibility is when you have the ability to interrupt an animation in flight so you can stop, change, or restart it. Normally, with UIView block-based animations, or Core Animation based animations, this is really difficult to do (need to cancel the animation, figure out its current state on screen, apply that, etc.). UIViewPropertyAnimator works okay for this, but it relies heavily on "scrubbing" animations, which when working with physically-based animations (i.e. springs), that doesn't really make a lot of sense, since the physics are what generate the animation dynamically (vs. some predefined easing curve you can scrub).

Motion makes things like this easy, so you have to worry less about syncing up animation state with gestures, and focus more about the interactions themselves.

Here's an example of how a drag to a spring animation and then catching and redirecting that animation could look like:

Let's say you have a subview view inside another view (self).

// Create a spring animation configured with our constants.
var springAnimation: SpringAnimation<CGPoint>()
springAnimation.configure(response: 0.30, damping: 0.64)

// When you drag on the view and let go, it'll spring away from the center and then rebound back.
// At any point, you can grab the view and do it again.
func didPan(_ gestureRecognizer: UIPanGestureRecognizer) {
    switch gestureRecognizer.state: {
        case .began:
            springAnimation.stop()
        case .changed:
            view.center = gestureRecognizer.location(in: self)
            springAnimation.updateValue(to: view.center)
        case .ended:
            springAnimation.toValue = self.center
            springAnimation.velocity = gestureRecognizer.velocity(in: self)
            springAnimation.onValueChanged { newValue in
                view.center = newValue
            }
            springAnimation.start()
    }
}

Note: You can try this out in the example project (under the Dragging Demo).

SIMD

SIMD powers a lot of how Motion works and avoids having to use more "expensive" objects like NSValue or NSNumber to animate. SIMD grants the ability to pack multiple values into a single SIMD register and then perform math on all those values simultaneously (Single Instruction Multiple Data). This means you can do neat things like animate a CGRect to another CGRect in a single super fast operation (rather than 4 separate operations: x, y,, width, height). It's not always the silver bullet, but on average, it's at least on par, and often faster than the naive implementation.

Motion exposes a protocol called SIMDRepresentable that allows for easy boxing and unboxing of values:

let point = CGPoint(x: 10.0, y: 10.0)
let simdPoint: SIMD2<CGFloat.NativeType> = point.simdRepresentation()
let pointBoxedAgain = CGPoint(simdPoint)

These conversions are relatively inexpensive, and Motion has been heavily optimized to avoid copying or boxing/unboxing them whenever it can.

For more information on SIMD, check out the docs.

Performance

Motion is pretty dang fast (especially on Apple Silicon!), leveraging some manual Swift optimization / specialization as well as SIMD it's capable of executing 5000 SpringAnimation<SIMD64<Double>> in ~130ms on an iPhone 12 Pro (that's 320,000 springs at 0.4 m

View on GitHub
GitHub Stars1.5k
CategoryDevelopment
Updated9d ago
Forks37

Languages

Swift

Security Score

95/100

Audited on Mar 22, 2026

No findings