The animation(_:body:) modifier scopes an animation to a ViewBuilder closure, so it animates
only the modifiers inside that block. It solves both problems of the old forms: animating
unrelated changes, and stacking one animation(value:) per property.
Why the Old Forms Leak
A bare animation(.default) on a container animates every change inside it, including ones you
never intended, such as an environment value change. Binding with value: fixes that but scopes
to one value only.
VStack {
Button("Animate") { isHidden.toggle() }
HugeView().opacity(isHidden ? 0.0 : 1.0)
AnotherHugeView()
}
.animation(.default, value: isHidden)
With several animatable properties, the value: form needs one modifier per property, which is
correct but clumsy.
The Scoped Form
The closure variant applies the animation only to the modifiers written inside the closure. The
content parameter is a placeholder for the view the modifier is attached to.
SomeView()
.animation(.default) { content in
content
.opacity(firstStep ? 1.0 : 0.0)
.blur(radius: secondStep ? 0 : 20.0)
}
Any state change that touches those two modifiers animates. Nothing outside the closure picks the animation up, so the rest of the hierarchy stays still.
Scoped Transactions
The transaction(_:body:) modifier does the same for transactions: mutate the transaction, and
the change applies only inside the body closure.
SomeView()
.transaction { t in
t.animation = t.animation?.speed(2)
} body: { content in
content
.opacity(firstStep ? 1.0 : 0.0)
.blur(radius: secondStep ? 0 : 20.0)
}
Both scoped forms shipped with the iOS 17 generation of platforms and are not backward
compatible. On earlier targets, fall back to animation(_:value:) per property.