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.