How to use animation with value in SwiftUI (iOS 16+) | SwiftUI Bootcamp #67
The old .animation(.spring()) modifier animated every state change that hit a view — even changes you didn't intend to animate. The value-based .animation(.spring(), value: someState) introduced in iOS 16 fixes this by making the link between a specific state change and the animation it triggers explicit and intentional.
What You'll Learn
- Why
.animation(_:value:)is preferred over the deprecated parameter-less.animation(_:) - How to animate two different state values with two different animation curves independently
- How SwiftUI determines which visual changes to animate when a specific value changes
Mental Model
The old animation modifier was like installing a motion sensor that triggers every light in the house — any movement anywhere would trigger all the lights. The value-based .animation(_:value:) is like installing dedicated motion sensors per room: "this sensor only triggers the kitchen lights, and this one only triggers the bedroom lights."
Each .animation(_:value:) modifier watches one specific value. When that value changes, only the visual changes caused by that value are animated — not every other state change happening at the same time. Two different animation curves can run simultaneously on the same view hierarchy, each tied to a different value.
Detailed Explanation
.animation(_:value:) requires iOS 16+ (Xcode 14+). It takes two parameters: the animation curve to apply, and the value to watch. The modifier observes the value using Equatable conformance — when the value changes between two render cycles, SwiftUI animates the diff. If the value hasn't changed, the modifier does nothing.
The distinction between .spring() and .linear(duration:) matters in practice. Spring animations feel physical because they can overshoot their target and bounce — ideal for position changes and size changes that should feel elastic. Linear animations move at a constant rate — useful for progress indicators, loading bars, or any animation where a steady pace communicates reliability.
The commented-out .animation(.spring()) at the bottom is the deprecated form. It was deprecated because it would animate literally every state change on that view, including changes triggered by parent re-renders, making the behavior unpredictable and sometimes causing unwanted animations on unrelated state changes.
Minimum deployment target: iOS 16 is required for the value-based .animation. If you need to support iOS 15, use withAnimation { } around your state change as an alternative — it achieves similar scoping.
Code Structure
AnimationUpdatedBootcamp.swift contains one view with two independent boolean states (animate1, animate2) that control the position of a rectangle along horizontal and vertical axes. Two separate .animation modifiers are applied with different curves — this is the key demonstration: each animation curve is linked to exactly one state value.
Complete Code
AnimationUpdatedBootcamp.swift
import SwiftUI
struct AnimationUpdatedBootcamp: View {
@State private var animate1: Bool = false
@State private var animate2: Bool = false
var body: some View {
ZStack {
VStack(spacing: 40) {
Button("Action 1") {
animate1.toggle() // Triggers the spring animation below
}
Button("Action 2") {
animate2.toggle() // Triggers the linear animation below
}
ZStack {
Rectangle()
.frame(width: 100, height: 100)
// animate1 controls horizontal position — leading or trailing
.frame(maxWidth: .infinity, alignment: animate1 ? .leading : .trailing)
.background(Color.green)
// animate2 controls vertical position — top or bottom
.frame(maxHeight: .infinity, alignment: animate2 ? .top : .bottom)
.background(Color.orange)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
}
}
// Only animates changes caused by animate1 flipping — uses a bouncy spring curve
.animation(.spring(), value: animate1)
// Only animates changes caused by animate2 flipping — uses a slow, steady linear curve
.animation(.linear(duration: 5), value: animate2)
// deprecated!
// .animation(.spring())
}
}
struct AnimationUpdatedBootcamp_Previews: PreviewProvider {
static var previews: some View {
AnimationUpdatedBootcamp()
}
}Code Walkthrough
@State private var animate1: Bool = falseandanimate2— Two separate boolean values each drive a different visual change. The key lesson: keeping them separate lets you attach different animations to each..frame(maxWidth: .infinity, alignment: animate1 ? .leading : .trailing)— The rectangle slides left or right based onanimate1. SwiftUI detects the alignment change as a visual diff and animates it whenanimate1changes..frame(maxHeight: .infinity, alignment: animate2 ? .top : .bottom)— The rectangle moves up or down based onanimate2. This is a second independent visual change — SwiftUI stacks the two axis movements..animation(.spring(), value: animate1)— This modifier watches onlyanimate1. When it flips, the horizontal position change is animated with a spring. The vertical movement (driven byanimate2) is not affected by this modifier..animation(.linear(duration: 5), value: animate2)— Watches onlyanimate2. Vertical moves take 5 seconds at a constant rate. This shows the power of independent animation: tap "Action 1" and the rectangle springs left/right instantly; tap "Action 2" and it glides up/down over 5 seconds.// deprecated! .animation(.spring())— The old modifier. It would apply spring animation to every state change on this view — includinganimate2's vertical movement, meaning you could never give them different curves.
Common Mistakes
Mistake: Using the deprecated .animation(_:) without a value parameter
The deprecation warning exists for a reason. Without a value, the modifier animates everything, including re-renders triggered by parent views. This leads to unexpected animations and is hard to debug. Always provide a value: parameter.
Mistake: Animating with .animation(_:value:) but forgetting the state variable needs to actually change
The animation only fires when the watched value changes. Setting animate1 = false when it's already false does nothing — no animation plays. This is by design, but can confuse learners who expect an animation to replay on every button tap.
Mistake: Placing .animation inside the view hierarchy rather than on the root
Placing .animation(.spring(), value: animate1) on an inner view (like directly on the Rectangle) limits which parts of the view hierarchy it animates. Placing it on the outer container (as done here) means all visual changes anywhere inside that container that result from animate1 changing are animated.
Key Takeaways
.animation(_:value:)is the modern API (iOS 16+) that links a specific animation curve to a specific state value — avoiding the "animate everything" behavior of the deprecated.animation(_:)- Multiple
.animationmodifiers can coexist on the same view, each watching a different value and using a different curve - Use
withAnimation { }around a state mutation as an equivalent alternative when you need to support iOS 15
Last updated: June 27, 2026