Home

Implicit and explicit

Two ways to attach an animation, and making your own values animatable

There are two places an animation can be attached: to the change, or to the view.

Explicit wraps the state change and animates everything affected by it:

withAnimation(.spring(duration: 0.4)) {
    isExpanded.toggle()
}

Implicit marks a view as animating when a particular value changes:

CardView()
    .rotationEffect(.degrees(isExpanded ? 180 : 0))
    .animation(.spring(duration: 0.4), value: isExpanded)

The difference is scope. withAnimation animates every view in the app that depends on that change; .animation(_:value:) animates only this view, and only when isExpanded changes.

Prefer implicit when a view has its own idea of how it should move — a chevron that always rotates with the same spring, wherever it is used. Prefer explicit when one gesture should move several things together, which is the more common case in practice.

The value: parameter is not optional any more

The original .animation(_:) — with no value — animated any change the view could see. It was removed because it was almost always wrong: a view redrawing for an unrelated reason would animate its position, so lists slid around when a scroll happened and text crossfaded when a number elsewhere changed.

.animation(.default)                    // deprecated: animates everything, unpredictably
.animation(.default, value: isOn)       // animates only when isOn changes

If you are working in a codebase that still has the old spelling, that is worth fixing on sight — it is a reliable source of animations nobody asked for.

Tip

.animation(nil, value:) switches animation off for a subtree. It is the tool for the one child that should snap while everything around it animates, and it is much easier to reason about than trying to scope a withAnimation to exclude something.

What is actually being interpolated

SwiftUI can only animate a value that knows how to be halfway. That is the Animatable protocol, and it has exactly one requirement:

protocol Animatable {
    associatedtype AnimatableData: VectorArithmetic
    var animatableData: AnimatableData { get set }
}

CGFloat, Double, CGSize, CGPoint, Color, EdgeInsets and Angle all conform, which is why offsets, opacities, frames and colours animate for free. Anything that does not conform cannot be interpolated, and changes to it happen instantly in the middle of an otherwise smooth animation.

That is the answer to “why did the text jump while everything else slid?” — a String has no halfway.

Making a custom value animatable

The case where this matters is a Shape driven by a number. Without animatableData, the shape redraws once at the end rather than being redrawn at each step:

struct Wedge: Shape {
    var endAngle: Angle

    var animatableData: Double {
        get { endAngle.degrees }
        set { endAngle = .degrees(newValue) }
    }

    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.midX, y: rect.midY))
        path.addArc(center: CGPoint(x: rect.midX, y: rect.midY),
                    radius: rect.width / 2,
                    startAngle: .zero, endAngle: endAngle, clockwise: false)
        return path
    }
}

Now Wedge(endAngle: .degrees(progress * 360)) sweeps, because SwiftUI sets animatableData sixty times a second and calls path(in:) for each value.

For two values at once, use AnimatablePair:

var animatableData: AnimatablePair<Double, Double> {
    get { AnimatablePair(width, height) }
    set { width = newValue.first; height = newValue.second }
}

Transactions

A transaction is the animation context travelling with a change. withAnimation is a convenience that puts an Animation into the current transaction; reading it lets a view see how it is being animated and respond:

.transaction { transaction in
    transaction.animation = transaction.disablesAnimations ? nil : .spring
}

The practical use is overriding an animation imposed from above. A parent wrapping everything in withAnimation(.easeInOut(duration: 2)) will drag your view along with it; a .transaction block on your view is how it declines, and it is the only thing that works when you do not control the call site.

Warning

.transaction applies to every change flowing through that view, which makes it the blunt instrument of this chapter. Reach for .animation(_:value:) first and keep .transaction for the case where the animation is being set by code you cannot change.