WWDC 2026 deal: 30% off our Swift and SwiftUI books! Learn more ...WWDC 2026 deal:30% off our Swift and SwiftUI books >>

SwiftUI animation timing

The Animation type in SwiftUI describes the timing curve for a value change, controlling how quickly a value moves toward its destination and whether it overshoots before settling. We can apply it to a specific view using the animation(_:value:) modifier, or wrap a state change in withAnimation(::) to animate all affected views at once.

Rectangle()
    .frame(width: 100, height: 100)
    .scaleEffect(isExpanded ? 2 : 1)
    .animation(.default, value: isExpanded)

Since iOS 17, the default option is a spring animation with a configuration chosen by Apple. We can replace it with any other Animation value when we need more control over the timing. In this post we will look at the easing and spring animations that SwiftUI provides, as well as the CustomAnimation protocol for fully custom timing logic.

# Easing animations

Easing animations describe their timing curve using a cubic Bezier curve, which is defined by four points: two fixed endpoints at (0, 0) and (1, 1), and two control points that shape the curve between them. The position of the control points determines how the animation accelerates and decelerates, giving each preset its distinct character.

Cubic Bezier timing curve A graph with time on the horizontal axis and position on the vertical axis, showing an ease-in-out curve from (0,0) to (1,1). Two control points shape the curve: control point 1 at (0.42, 0.00) near the start, and control point 2 at (0.58, 1.00) near the end.

SwiftUI provides four named presets for easing animations, each corresponding to a specific control point configuration. Each preset is available as a static property with a default duration, and as a static method that accepts an explicit duration in seconds.

The linear preset places both control points directly on the endpoints, which means they have no pull on the curve at all, producing a constant slope throughout. The animation moves at the same speed from start to finish, which reads as mechanical in most UI contexts but is the right choice for things like a continuously rotating indicator where evenness is the point.

linear timing curve A straight diagonal line from (0,0) to (1,1), showing constant speed throughout. Both control points coincide with the endpoints.
Rectangle()
    .frame(width: 250, height: 250)
    .rotationEffect(.degrees(isAnimating ? 360 : 0))
    .animation(.linear(duration: 6), value: isAnimating)
A rectangle rotating continuously at constant speed, demonstrating the linear animation preset iPhone 16 Frame

The easeIn preset moves the first control point to the right, flattening the start of the curve. The animation begins slowly and accelerates toward the end, which suits elements that are leaving the screen.

easeIn timing curve A curve that starts shallow and becomes steep, showing slow start accelerating to the end. Control point 1 is at (0.42, 0.00) and control point 2 is at (1.00, 1.00).
Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : 340)
    .animation(.easeIn(duration: 2), value: isVisible)
A rectangle sliding off screen slowly at first then accelerating, demonstrating the easeIn animation preset iPhone 16 Frame

The easeOut preset moves the second control point to the left, flattening the end. The animation arrives quickly and decelerates to a stop. This is the most commonly useful preset for elements entering or settling into a final position.

easeOut timing curve A curve that starts steep and flattens toward the end, showing fast start decelerating to a stop. Control point 1 is at (0.00, 0.00) and control point 2 is at (0.58, 1.00).
Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(.easeOut(duration: 2), value: isVisible)
A rectangle sliding onto the screen quickly then decelerating to a stop, demonstrating the easeOut animation preset iPhone 16 Frame

The easeInOut preset moves both control points inward, producing a curve that is shallow at both ends and steep in the middle. It works well for elements that move between two positions on screen, where both the departure and arrival benefit from a softer edge. Before iOS 17, this was the SwiftUI default.

easeInOut timing curve An S-shaped curve that is shallow at both ends and steep in the middle, showing slow start, fast middle, and slow end. Control point 1 is at (0.42, 0.00) and control point 2 is at (0.58, 1.00).
Rectangle()
    .frame(width: 250, height: 250)
    .offset(y: hasMoved ? 200 : -200)
    .animation(.easeInOut(duration: 2), value: hasMoved)
A rectangle moving between two positions, starting slowly, speeding up through the middle, and decelerating to a stop, demonstrating the easeInOut animation preset iPhone 16 Frame

# Custom timing curves

When the four named presets do not produce the exact curve we need, the timingCurve(_:duration:) method lets us specify a custom one. Since iOS 17, it takes a UnitCurve value, which is a type that represents a timing curve independently of any animation. We create a custom curve using bezier(startControlPoint:endControlPoint:), passing two UnitPoint values that define the control points of the cubic Bezier curve. The start control point influences how the curve leaves the origin, and the end control point influences how it arrives at the destination.

Custom Bezier timing curve A custom cubic Bezier curve with start control point at (0.2, 0.8) and end control point at (0.8, 0.2), producing a curve that rises quickly at the start then flattens.
Rectangle()
    .frame(width: 250, height: 250)
    .offset(y: hasMoved ? 200 : -200)
    .animation(
        .timingCurve(.bezier(
            startControlPoint: UnitPoint(x: 0.2, y: 0.8),
            endControlPoint: UnitPoint(x: 0.8, y: 0.2)
        ), duration: 2),
        value: hasMoved
    )
A rectangle moving with a custom Bezier timing curve, demonstrating the timingCurve API iPhone 16 Frame

Prior to iOS 17, the same result was achievable using timingCurve(_:_:_:_:duration:), which takes the four control point values as raw Double arguments directly.

UnitCurve also comes with its own named presets that we can pass directly to timingCurve(_:duration:). The Bezier-based ones mirror the Animation presets we already covered: easeIn, easeOut, and easeInOut.

Alongside those, UnitCurve provides three circular variants: circularEaseIn, circularEaseOut, and circularEaseInOut. Where the Bezier-based curves are defined by control points, the circular variants use a quarter circle arc as their underlying shape. This produces a more aggressive acceleration and deceleration than their Bezier equivalents, which can feel snappier in practice.

circularEaseInOut compared to easeInOut Two S-shaped curves overlaid: easeInOut in blue and circularEaseInOut in orange. The circular curve is flatter at both ends and steeper in the middle, showing more aggressive acceleration and deceleration.
Rectangle()
    .frame(width: 250, height: 250)
    .offset(y: hasMoved ? 200 : -200)
    .animation(
        .timingCurve(.circularEaseInOut, duration: 2),
        value: hasMoved
    )
A rectangle moving between two positions with a circular easeInOut curve, staying flat at both ends and moving sharply through the middle iPhone 16 Frame

# Spring animations

Unlike easing animations, which follow a fixed mathematical curve, spring animations simulate a physical object attached to a spring. On every frame, SwiftUI applies two forces: the spring force pulls the object toward its destination, and the damping force opposes its motion.

Spring forces A box attached to a spring, displaced above its rest position. The spring force arrow points downward toward the target. The damping force arrow points upward, opposing the direction of motion.

The balance between these forces determines the character of the animation. When the damping force is strong enough, the object reaches its destination without overshooting. When it is weaker, the object overshoots and oscillates back and forth before settling. SwiftUI provides three named presets, smooth, snappy, and bouncy, that correspond to specific points along this spectrum.

The smooth preset is a critically damped spring: the damping force is exactly strong enough to prevent any overshoot. The object reaches its destination as quickly as possible without crossing it, producing a clean, settled arrival.

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(.smooth, value: isVisible)
A rectangle animating onto the screen with the smooth spring preset, settling without any overshoot iPhone 16 Frame

The snappy preset is slightly underdamped, producing a small overshoot before settling. The subtle elasticity gives it a more lively feel than smooth.

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(.snappy, value: isVisible)
A rectangle animating onto the screen with the snappy spring preset, with a subtle overshoot before settling iPhone 16 Frame

The bouncy preset is more underdamped, producing a clearly visible overshoot and a brief oscillation before settling. It draws attention to the motion itself and suits moments where a playful, energetic feel is appropriate.

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(.bouncy, value: isVisible)
A rectangle animating onto the screen with the bouncy spring preset, overshooting and oscillating visibly before settling iPhone 16 Frame

# Customizing spring animations

SwiftUI provides several ways to customize the character of a spring animation, from adjusting the built-in presets to specifying the spring parameters directly.

Each of the three spring presets discussed earlier has a corresponding static method that accepts a duration and extraBounce parameter. The duration parameter controls how long the spring takes to settle, and extraBounce adds on top of the preset's default bounce value. A small positive value nudges a preset slightly bouncier, while a larger one can make bouncy feel even more energetic.

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(
        .bouncy(duration: 0.5, extraBounce: 0.2),
        value: isVisible
    )
A rectangle animating with a customized bouncy spring, showing a more pronounced bounce than the default preset iPhone 16 Frame

When the presets do not provide enough control, spring(duration:bounce:blendDuration:) and spring(response:dampingFraction:blendDuration:) let us define the spring character directly, using either the duration/bounce model or the response/dampingFraction model. For gesture-driven interactions, interactiveSpring(response:dampingFraction:blendDuration:) offers defaults tuned to feel nearly instant, while interpolatingSpring(duration:bounce:initialVelocity:) accumulates the effects of overlapping animations rather than replacing them.

# Custom animations

In situations where we cannot achieve the desired animation through the built-in timing options, we can use the CustomAnimation protocol, available from iOS 17, to implement the timing logic ourselves. The protocol has one required method, animate(value:time:context:), which SwiftUI calls on every frame with the value being animated and the time elapsed since the animation started, expecting back the interpolated value at that moment, or nil once the animation is complete.

The following example uses CustomAnimation to implement a slingshot curve, where the view briefly pulls back before shooting toward its destination.

struct SlingshotAnimation: CustomAnimation {
    let duration: TimeInterval = 0.6

    func animate<V: VectorArithmetic>(
        value: V,
        time: TimeInterval,
        context: inout AnimationContext<V>
    ) -> V? {
        guard time <= duration else { return nil }
        let t = time / duration
        let curve = t * t * (3 - 2 * t) + sin(t * .pi) * -0.3
        return value.scaled(by: curve)
    }
}

extension Animation {
    static var slingshot: Animation {
        Animation(SlingshotAnimation())
    }
}

Wrapping the conforming type in an Animation extension makes it available as a static property, the same pattern used by the built-in presets.

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : 340)
    .animation(.slingshot, value: isVisible)
A rectangle dismissing off screen with a slingshot curve, briefly pulling back before shooting away iPhone 16 Frame

# Animation modifiers

All Animation values, whether easing, spring, or custom, support a set of modifiers that adjust their behavior without changing their underlying timing curve. These modifiers chain directly onto any animation value and can be combined in any order.

The delay(_:) modifier postpones the start of the animation by a given number of seconds, which is useful when staggering multiple elements so each one begins slightly after the previous one.

VStack(spacing: 16) {
    Rectangle()
        .frame(width: 250, height: 80)
        .offset(x: showRects ? 0 : -340)
        .animation(.smooth, value: showRects)

    Rectangle()
        .frame(width: 250, height: 80)
        .offset(x: showRects ? 0 : -340)
        .animation(.smooth.delay(0.15), value: showRects)

    Rectangle()
        .frame(width: 250, height: 80)
        .offset(x: showRects ? 0 : -340)
        .animation(.smooth.delay(0.3), value: showRects)
}
Three rectangles sliding in from the left with a staggered delay, each one starting slightly after the previous iPhone 16 Frame

The speed(_:) modifier scales the duration of the animation by a factor, where 2.0 makes it run twice as fast and 0.5 makes it run at half speed. It is useful for reusing the same animation curve at different speeds without defining a new one.

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(.bouncy.speed(2), value: isVisible)
A rectangle sliding in from the left at double speed using the bouncy spring preset iPhone 16 Frame

The repeatCount(_:autoreverses:) modifier runs the animation a specific number of times. When autoreverses is true, the animation alternates direction on each repetition. When it is false, the animated value jumps back to its start position between each cycle.

Rectangle()
    .frame(width: 250, height: 250)
    .scaleEffect(isAnimating ? 1.3 : 1)
    .animation(
        .easeInOut(duration: 0.4).repeatCount(3, autoreverses: true),
        value: isAnimating
    )
A rectangle scaling up and back down three times using the easeInOut animation iPhone 16 Frame

The repeatForever(autoreverses:) modifier works the same way but continues indefinitely. Setting autoreverses to false is common for continuous looping animations, like a rotating indicator, where the motion should cycle seamlessly.

Rectangle()
    .frame(width: 250, height: 250)
    .rotationEffect(.degrees(isAnimating ? 360 : 0))
    .animation(
        .linear(duration: 4).repeatForever(autoreverses: false),
        value: isAnimating
    )
A rectangle rotating continuously using a linear animation that repeats forever iPhone 16 Frame


Animation timing in SwiftUI covers more ground than it might initially appear. The built-in options alone span a wide range of motion models, and the ability to implement custom timing logic or compose modifiers on top of any animation gives us a lot of room to work with. Knowing what each option models and when to reach for it makes it easier to produce motion that feels considered.


If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects. And my new book The SwiftUI Way helps you adopt recommended patterns, avoid common pitfalls, and use SwiftUI's native tools appropriately to work with the framework rather than against it.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

I’m currently running a WWDC 2026 promotion with 30% off my books, plus additional savings when purchased as a bundle. Visit the books page to learn more.

The SwiftUI Way by Natalia Panferova book coverThe SwiftUI Way by Natalia Panferova book cover

WWDC 2026 offer: 30% off!$35$25

A field guide to SwiftUI patterns and anti-patterns

The SwiftUI Wayby Natalia Panferova

  • Avoid common SwiftUI pitfalls
  • Build deeper intuition for the framework
  • Gain insights from a former SwiftUI Engineer at Apple
WWDC 2026 offer: 30% off!

A field guide to SwiftUI patterns and anti-patterns

The SwiftUI Way by Natalia Panferova book coverThe SwiftUI Way by Natalia Panferova book cover

The SwiftUI Way

by Natalia Panferova

$35$25