Adding Animations in SwiftUI | SwiftUI Bootcamp #25
In most frameworks, adding animation means writing animation code separately from layout code. SwiftUI takes a completely different approach: you describe the end state, attach .animation() to the view, and SwiftUI figures out how to smoothly interpolate between states. This lesson shows you how that system works.
What You'll Learn
- How the
.animation()modifier attaches an animation to a view's state-driven changes - How
repeatForever(autoreverses:)creates a continuous looping animation - Why the placement of
.animation()in the modifier chain determines which properties are animated
Mental Model
Think of SwiftUI animation like a GPS navigation system. You tell the GPS your destination (the new state), and it figures out how to get there smoothly. Without animation, SwiftUI teleports from A to B instantly. With animation, it drives you there — following the road (the animation curve), at a chosen speed (duration), and possibly making the round trip automatically (repeatForever).
The view doesn't know it's being animated. It just knows its current values. SwiftUI watches for value changes and interpolates automatically between the old and new values over time. You aren't writing "move 10 pixels per frame" — you're saying "get to the destination smoothly."
Detailed Explanation
SwiftUI animation is built on a simple rule: when a @State value changes, any view that has .animation() attached will interpolate its animatable properties between the old and new values. "Animatable properties" are things like size, color, rotation, offset, and opacity — values that can be meaningfully blended over time.
The .animation() modifier takes an Animation value, which describes the timing curve and duration. .default uses a spring animation. Animation.linear(duration:), Animation.easeIn, Animation.easeInOut, and Animation.spring(...) are the most common choices. Lesson 26 dives deep into these options.
repeatForever(autoreverses: true) is a modifier on the Animation itself (not on the view). It instructs SwiftUI to repeat the animation indefinitely, playing it forward and then backward. This is how you create "breathing" or "pulsing" effects without managing a timer.
The position of .animation() in the modifier chain matters significantly. In older SwiftUI (iOS 13/14), .animation() placed directly on a view animates all animatable properties of that view. This can cause unintended animations when you only meant to animate one property. The preferred modern approach is withAnimation { stateChange } at the call site (the button action), which gives you explicit control over what triggers the animation.
Code Structure
AnimationBootcamp.swift demonstrates the legacy .animation() modifier-on-view approach. The @available annotation at the top is significant — it marks this code as using an older API pattern. The single isAnimated flag simultaneously drives corner radius, color, size, rotation, and vertical offset, all animated together.
Complete Code
AnimationBootcamp.swift
import SwiftUI
@available(*, introduced: 13.0, deprecated: 15.0, message: "Use AnimationUpdatedBootcamp instead.") // marks that .animation(_:) on a view was deprecated in iOS 15 in favor of .animation(_:value:)
struct AnimationBootcamp: View {
@State var isAnimated: Bool = false // single flag drives all animated properties
var body: some View {
VStack {
Button("Button") {
isAnimated.toggle() // toggling this flag causes all ternaries below to re-evaluate
}
Spacer()
RoundedRectangle(cornerRadius: isAnimated ? 50 : 25) // corner radius animates between 25 and 50
.fill(isAnimated ? Color.red : Color.green) // color interpolates between green and red
.animation(Animation
.default // uses SwiftUI's default spring animation
.repeatForever(autoreverses: true)) // loops forever, reversing direction each cycle
.frame(
width: isAnimated ? 100 : 300, // width shrinks dramatically when animated
height: isAnimated ? 100 : 300) // height follows the same scale-down
.rotationEffect(Angle(degrees: isAnimated ? 360 : 0)) // rotates a full circle
.offset(y: isAnimated ? 300 : 0) // shifts downward when isAnimated is true
Spacer()
}
}
}
struct AnimationBootcamp_Previews: PreviewProvider {
static var previews: some View {
AnimationBootcamp()
}
}Code Walkthrough
@available(*, deprecated: 15.0, ...)— This annotation is educational: it signals that.animation(_:)placed directly on a view was soft-deprecated in iOS 15. The modern replacement is.animation(_:value:)orwithAnimation { }. The lesson is still valid for understanding the concept.isAnimated.toggle()— One Boolean flip causes every ternary in the view to evaluate its new branch. SwiftUI compares old values to new values, finds the differences, and animates them..animation(Animation.default.repeatForever(autoreverses: true))— This modifier is placed above.frame(),.rotationEffect(), and.offset(). In this position, it animates the corner radius and fill color (modifiers above it) AND all properties on the view (in older SwiftUI behavior). This is a known quirk of modifier-position sensitivity.Animation.default— SwiftUI's default spring animation. It produces a subtle bounce effect that feels natural for UI interactions. You can swap this with.linear,.easeInOut, or.spring(...)to change the feel..repeatForever(autoreverses: true)— Without this, the animation plays once to theisAnimated = truestate and stops. With it, SwiftUI continuously plays forward (to the true values), then backward (to the false values), creating a loop.Multiple animated properties — Corner radius, color, width, height, rotation, and Y-offset all animate simultaneously from the same flag change. This is the power of SwiftUI's approach: you describe both states and it handles the interpolation for all of them.
Common Mistakes
Mistake: Placing .animation() on the wrong view or in the wrong position.animation() on a container animates the container's properties, not necessarily the children's. If your animation isn't working, check that the modifier is on the view whose properties are changing, not on a parent wrapper.
Mistake: Using .animation() without specifying a value in modern SwiftUI
In iOS 15+, animation(_:) without a value: parameter triggers a deprecation warning. Use .animation(.default, value: isAnimated) to explicitly declare which state change triggers the animation.
Mistake: Expecting repeatForever to start automatically on view appearancerepeatForever only starts animating when the state changes. If you want an animation to start immediately when the view appears, trigger it in .onAppear { withAnimation { isAnimated = true } }.
Key Takeaways
- SwiftUI animation works by interpolating between old and new values of animatable properties — you define the destination, not the motion path.
.animation(_:)on a view tells SwiftUI to animate that view's property changes using the specified animation curve.repeatForever(autoreverses:)creates continuous looping animations without timers or manual state management.
Last updated: June 27, 2026