Animations & Transitions
Reviewed & published by Brayan K
SwiftUI makes motion almost free: change state and the framework interpolates the difference for you. You'll animate changes implicitly with the .animation modifier and explicitly with withAnimation, animate views in and out with transitions, and create hero effects with matchedGeometryEffect.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Building iOS Apps with SwiftUI • Intermediate
What You'll Learn in This Lesson
- • Animate implicitly with .animation(_:value:)
- • Animate explicitly with withAnimation
- • Choose curves: .easeInOut, .spring()
- • Animate insertion/removal with .transition
- • Build hero effects with matchedGeometryEffect
1️⃣ Implicit Animation — the .animation Modifier
Attach .animation(_:value:) to a view and it animates whenever the tracked value changes. You describe the start and end states; SwiftUI tweens between them. Great for self-contained effects like a button pulse.
import SwiftUI
// IMPLICIT animation: attach .animation to a view and it animates whenever the
// value it depends on changes. SwiftUI interpolates the change for you.
struct PulsingHeart: View {
@State private var big = false
var body: some View {
Image(systemName: "heart.fill")
.scaleEffect(big ? 1.5 : 1.0)
// animate the scale change with an ease-in-out curve
.animation(.easeInOut(duration: 0.3), value: big)
.onTapGesture { big.toggle() }
}
}2️⃣ Explicit Animation — withAnimation
Wrap the state change in withAnimation and SwiftUI animates every view affected by that change, together, with the curve you choose. Use .spring() for bouncy, natural motion.
import SwiftUI
// EXPLICIT animation: wrap the STATE CHANGE in withAnimation. Only the changes
// caused inside the closure are animated, with the curve you choose.
struct ExpandingBox: View {
@State private var expanded = false
var body: some View {
VStack {
Rectangle()
.fill(Color.blue)
.frame(width: expanded ? 200 : 80, height: 80)
Button("Toggle") {
withAnimation(.spring()) { // bouncy, physics-based motion
expanded.toggle()
}
}
}
}
}3️⃣ Transitions — Animating In & Out
A .transition controls how a view appears and disappears when it's added to or removed from the hierarchy. It only plays when that insertion/removal is part of an animated change — typically toggling an if inside withAnimation.
import SwiftUI
// A TRANSITION controls how a view enters and leaves the hierarchy. It plays
// when the insertion/removal happens inside an animated state change.
struct BannerDemo: View {
@State private var showBanner = false
var body: some View {
VStack {
if showBanner {
Text("Saved!")
.padding()
.background(Color.green.opacity(0.3))
.transition(.move(edge: .top)) // slide in from the top
}
Button("Toggle banner") {
withAnimation {
showBanner.toggle() // drives the transition
}
}
}
}
}4️⃣ matchedGeometryEffect — Hero Animations
Give two views the same id within a shared @Namespace and SwiftUI animates a seamless morph between their frames — the classic "thumbnail grows into a full image" hero effect.
import SwiftUI
// matchedGeometryEffect animates a view smoothly between two layouts that
// share the SAME id inside a SHARED Namespace.
struct HeroToggle: View {
@Namespace private var ns
@State private var zoomed = false
var body: some View {
VStack {
if zoomed {
Circle().fill(Color.orange)
.matchedGeometryEffect(id: "blob", in: ns)
.frame(width: 200, height: 200)
} else {
Circle().fill(Color.orange)
.matchedGeometryEffect(id: "blob", in: ns)
.frame(width: 60, height: 60)
}
Button("Zoom") { withAnimation { zoomed.toggle() } }
}
}
}Your turn. Fill in the blanks to animate a spinning gear both explicitly and implicitly.
// 🎯 YOUR TURN — fill in each ___ then preview in Xcode.
import SwiftUI
struct SpinButton: View {
@State private var angle = 0.0
var body: some View {
VStack {
Image(systemName: "gear")
.rotationEffect(.degrees(angle))
Button("Spin") {
// 1) Animate this change EXPLICITLY
___(.easeInOut) { // 👉 the function that animates a change
angle += 180
}
}
// 2) Add an IMPLICIT animation tied to 'angle' on the image above
// (write the modifier you'd attach):
// .___(.easeInOut, value: angle) // 👉 the implicit modifier
}
}
}Pro Tips
- 💡 Animate state, not views. You change a value; SwiftUI works out the motion.
- 💡 Pass value: to .animation so it only animates the change you mean.
- 💡 Try .spring() for natural, lively motion instead of stiff linear timing.
Common Errors (and the fix)
- Nothing animates — the change isn't animated. Wrap it in withAnimation or add .animation(_:value:).
- Transition pops instead of sliding — the insertion/removal wasn't inside an animated change. Toggle the condition inside withAnimation.
- matchedGeometryEffect jumps — the two views don't share the same id and @Namespace.
Mini-Challenge: Expanding Card
Build a card that toggles between a compact and an expanded state on tap. Use withAnimation(.spring()) to animate the size change, reveal an extra detail Text with a .transition(.opacity) when expanded, and bonus: use matchedGeometryEffect so an icon glides from the corner to the header as it grows.
🎉 Lesson Complete!
- ✅ Implicit: .animation(_:value:) on a view
- ✅ Explicit: wrap the change in withAnimation
- ✅ Curves: .easeInOut, .spring(), and more
- ✅ .transition animates views in and out; matchedGeometryEffect morphs between layouts
Practice quiz
What does withAnimation { ... } do?
- Pauses the app
- Animates the UI changes caused by the state change inside the closure
- Disables animation
- Logs to the console
Answer: Animates the UI changes caused by the state change inside the closure. withAnimation wraps a state change so SwiftUI animates the resulting view updates.
What is an IMPLICIT animation?
- An animation triggered by withAnimation
- An .animation modifier attached to a view that animates its changes
- A transition
- A timer
Answer: An .animation modifier attached to a view that animates its changes. An implicit animation uses the .animation modifier on a view to animate changes to its values automatically.
What is an EXPLICIT animation?
- The .animation modifier
- A static layout
- Wrapping a state change in withAnimation to animate that specific change
- A gradient
Answer: Wrapping a state change in withAnimation to animate that specific change. An explicit animation wraps the state change itself in withAnimation, animating exactly that update.
Which is a valid animation curve you can pass?
- .easeInOut
- .fastSlow
- .curve
- .smoothify
Answer: .easeInOut. .easeInOut is a built-in timing curve, alongside .linear, .easeIn, .easeOut, and .spring().
What does .spring() provide?
- A linear fade
- An instant change
- A delay only
- A bouncy, physics-based animation
Answer: A bouncy, physics-based animation. .spring() gives a natural, springy motion based on physics parameters.
What does a transition control?
- The font
- How a view appears and disappears when it is inserted or removed
- The list order
- The keyboard
Answer: How a view appears and disappears when it is inserted or removed. A .transition defines how a view animates in and out as it's added to or removed from the hierarchy.
Which modifier sets how a view enters/leaves the hierarchy?
- .transition(...)
- .frame(...)
- .padding(...)
- .tag(...)
Answer: .transition(...). .transition(...) specifies the insertion/removal animation, e.g. .opacity or .slide.
For a transition to play, the insertion/removal should be...
- Instant with no animation
- Driven by an animated state change (e.g. inside withAnimation)
- On a background thread
- Inside a Form
Answer: Driven by an animated state change (e.g. inside withAnimation). Transitions animate only when the add/remove is part of an animated state change.
What does matchedGeometryEffect enable?
- Matching fonts
- Smoothly animating a view as it moves between two layouts sharing an id
- Matching colors
- Geometry math only
Answer: Smoothly animating a view as it moves between two layouts sharing an id. matchedGeometryEffect links two views by id so SwiftUI animates a seamless transition between their frames.
matchedGeometryEffect requires which shared piece of setup?
- A common id and a Namespace
- A timer
- A NavigationStack
- A Form
Answer: A common id and a Namespace. You give both views the same id within a shared @Namespace so SwiftUI can match them.
Continue this course
- Previous: Forms & User Input
- Next: Custom View Modifiers & ViewBuilder — The ViewModifier protocol, custom modifiers and @ViewBuilder
Frequently asked questions
What is the difference between implicit and explicit animation?
An implicit animation uses the .animation(_:value:) modifier attached to a view: whenever the tracked value changes, SwiftUI animates that view's changes automatically. An explicit animation wraps the state change itself in withAnimation { ... }, animating exactly the updates caused inside that closure. Use implicit for self-contained view animations and explicit when one action should animate several views together.
What does withAnimation do?
withAnimation(_:) runs a closure that changes state, and tells SwiftUI to animate the view updates that result, using the timing curve you pass (or a default). It's the explicit approach: the animation is tied to the state change, not to a specific view, so every view affected by that change animates together.
What timing curves can I use?
SwiftUI offers .linear, .easeIn, .easeOut, .easeInOut, and physics-based .spring(). You can set a duration, e.g. .easeInOut(duration: 0.3), or tune a spring's response and damping. The curve controls how the animated value progresses over time, shaping how natural the motion feels.
What is a transition?
A transition defines how a view animates in and out as it's inserted into or removed from the view hierarchy — for example .opacity (fade), .slide, .scale, or .move(edge:). Apply it with the .transition(...) modifier. Crucially, the transition only plays when the insertion/removal is part of an animated state change, such as toggling an if condition inside withAnimation.
What is matchedGeometryEffect?
matchedGeometryEffect links two views that share the same id within a shared @Namespace, so when one is replaced by the other, SwiftUI animates a seamless transition between their positions and sizes. It powers hero animations — like a thumbnail growing into a full image — by morphing one frame into the other instead of cross-fading.