SwiftUI Animations & Transitions: A Complete Beginner's Guide
SwiftUI animations and transitions make your app feel responsive, polished, and easier to understand. In this article, you will learn how SwiftUI animates state changes, how transitions control how views appear and disappear, and how to avoid the most common mistakes that make animations seem like they are not working.
Quick answer: In SwiftUI, animations usually happen when state changes, and transitions define how a view enters or leaves the hierarchy. Use withAnimation for explicit animation and .transition for insert/remove effects.
Difficulty: Beginner
You'll understand this better if you know: basic Swift syntax, how @State works, and how SwiftUI updates views when data changes.
1. What Is SwiftUI Animations & Transitions?
SwiftUI animations are changes in view properties that happen over time instead of instantly. Transitions are a special kind of animation used when a view is inserted into or removed from the view hierarchy.
- Animations usually change size, position, opacity, rotation, color, or other animatable values.
- Transitions control how a view appears and disappears.
- SwiftUI often animates automatically when state changes, if the change is connected to an animation.
- You usually work with views, not manual frame-by-frame animation code.
In practice, animations and transitions are part of SwiftUI’s state-driven design: change the data, and the interface moves to match it.
2. Why SwiftUI Animations & Transitions Matter
Motion helps users understand what changed. A button that expands smoothly is easier to follow than one that suddenly jumps. A card that fades in feels connected to the rest of the interface instead of appearing out of nowhere.
SwiftUI animations matter because they:
- Guide attention to important changes.
- Make interface state easier to understand.
- Reduce the feeling of abrupt or broken updates.
- Help apps look more modern and refined with less code.
You should use them when the change is meaningful to the user, such as revealing content, toggling panels, moving between layouts, or confirming an action. Avoid animation when the change is purely informational and should happen instantly.
3. Basic Syntax or Core Idea
Animate a state change with withAnimation
The simplest way to animate in SwiftUI is to wrap a state change in withAnimation. Any animatable changes triggered by that state update will animate.
import SwiftUI
struct ContentView: View {
@State private var isExpanded = false
var body: some View {
VStack {
Rectangle()
.fill(Color.blue)
.frame(width: isExpanded ? 220 : 120,
height: 80)
.animation(.easeInOut(duration: 0.3), value: isExpanded)
Button("Toggle Size") {
withAnimation {
isExpanded.toggle()
}
}
}
.padding()
}
}This example changes the rectangle’s width. The animation modifier tells SwiftUI which state value to watch, and withAnimation makes the update animate when the button is tapped.
Show and hide a view with .transition
Transitions are most visible when a view is created or destroyed. You attach a transition to the view that appears or disappears.
import SwiftUI
struct TransitionDemo: View {
@State private var showMessage = false
var body: some View {
VStack(spacing: 20) {
Button("Toggle Message") {
withAnimation {
showMessage.toggle()
}
}
if showMessage {
Text("Hello, SwiftUI!")
.padding()
.background(Color.yellow)
.transition(.move(edge: .trailing))
}
}
.padding()
}
}When showMessage changes, SwiftUI inserts or removes the text. The transition describes how that insertion or removal should look.
4. Step-by-Step Examples
Example 1: Animate a scale change
This example makes a circle grow and shrink. The important part is that the view reads from state, so the animation can interpolate between the old and new values.
import SwiftUI
struct ScaleExample: View {
@State private var isLarge = false
var body: some View {
VStack {
Circle()
.fill(Color.green)
.frame(width: isLarge ? 180 : 80,
height: isLarge ? 180 : 80)
.animation(.spring(response: 0.4, dampingFraction: 0.7), value: isLarge)
Button("Toggle Size") {
isLarge.toggle()
}
}
.padding()
}
}This uses a spring animation, which feels natural for size changes. Notice that the state changes without withAnimation because the view already has an animation tied to isLarge.
Example 2: Animate opacity and position together
You can animate more than one property at once. SwiftUI animates any changes that are animatable and connected to the same update.
import SwiftUI
struct CardExample: View {
@State private var showCard = false
var body: some View {
VStack {
Button("Show Card") {
withAnimation(.easeInOut(duration: 0.25)) {
showCard.toggle()
}
}
if showCard {
RoundedRectangle(cornerRadius: 16)
.fill(Color.purple)
.frame(width: 260, height: 140)
.overlay(Text("Card Content").foregroundColor(.white))
.transition(.opacity.combined(with: .move(edge: .bottom)))
}
}
.padding()
}
}This combines opacity and movement so the card both fades and slides into view. Combined transitions are useful when a simple fade feels too plain.
Example 3: Animate a matched visual change inside layout
SwiftUI can animate layout changes too. If a view changes position because a stack or alignment changes, SwiftUI can move it smoothly.
import SwiftUI
struct AlignmentExample: View {
@State private var alignRight = false
var body: some View {
VStack(alignment: alignRight ? .trailing : .leading) {
Text("Aligned text")
.padding()
.background(Color.orange)
.animation(.easeInOut(duration: 0.35), value: alignRight)
Button("Change Alignment") {
alignRight.toggle()
}
}
.padding()
}
}Here, the view moves because its container alignment changes. This is a good example of SwiftUI animating layout, not just explicit view effects.
Example 4: Use asymmetric transitions for insertion and removal
Sometimes you want one effect when a view appears and a different one when it disappears. Asymmetric transitions handle that.
import SwiftUI
struct AsymmetricExample: View {
@State private var showBanner = false
var body: some View {
VStack(spacing: 16) {
Button("Toggle Banner") {
withAnimation(.easeOut(duration: 0.25)) {
showBanner.toggle()
}
}
if showBanner {
Text("Saved successfully")
.padding()
.background(Color.green.opacity(0.15))
.transition(
.asymmetric(
insertion: .move(edge: .top).combined(with: .opacity),
removal: .scale(scale: 0.9).combined(with: .opacity)
)
)
}
}
.padding()
}
}Asymmetric transitions are useful when the entry and exit should feel different, such as a message sliding in but shrinking away when dismissed.
5. Practical Use Cases
- Expanding and collapsing detail panels in a settings screen.
- Animating a selected tab indicator when the active tab changes.
- Showing success messages, alerts, or banners with a fade and slide transition.
- Moving cards in a dashboard when the layout changes from compact to expanded.
- Animating list row insertions and removals for filtered results.
- Making toggles, counters, and progress-related UI feel smoother and more readable.
These are all state-driven changes. If the interface can express the change with a value in your model, SwiftUI can usually animate it.
6. Common Mistakes
Mistake 1: Expecting a transition to work without conditional insertion
A transition only applies when a view enters or leaves the hierarchy. If the view is always present and only changes visibility with a modifier, the transition will not run the way you expect.
Problem: This code keeps the view in the hierarchy, so SwiftUI does not treat it as an insertion or removal event.
import SwiftUI
struct WrongTransition: View {
@State private var showText = false
var body: some View {
VStack {
Text("Fades in")
.opacity(showText ? 1 : 0)
.transition(.opacity)
Button("Toggle") {
showText.toggle()
}
}
}
}Fix: Put the view inside an if statement so it is inserted and removed.
import SwiftUI
struct CorrectTransition: View {
@State private var showText = false
var body: some View {
VStack {
if showText {
Text("Fades in")
.transition(.opacity)
}
Button("Toggle") {
withAnimation {
showText.toggle()
}
}
}
}
}The corrected version works because SwiftUI can now animate the view’s insertion and removal.
Mistake 2: Forgetting to wrap the state change in withAnimation
Some animations need an explicit animated transaction. If you change the state normally, the view updates instantly.
Problem: The state changes, but there is no animated transaction driving the update, so the interface jumps instead of animating.
import SwiftUI
struct NoAnimationTransaction: View {
@State private var isActive = false
var body: some View {
Circle()
.fill(Color.red)
.frame(width: isActive ? 180 : 80,
height: isActive ? 180 : 80)
.animation(.easeInOut(duration: 0.3), value: isActive)
.onTapGesture {
isActive.toggle()
}
}
}Fix: Wrap the state mutation in withAnimation when you want the update to animate immediately.
import SwiftUI
struct WithAnimationTransaction: View {
@State private var isActive = false
var body: some View {
Circle()
.fill(Color.red)
.frame(width: isActive ? 180 : 80,
height: isActive ? 180 : 80)
.animation(.easeInOut(duration: 0.3), value: isActive)
.onTapGesture {
withAnimation {
isActive.toggle()
}
}
}
}The corrected version works because the state change is now part of an animation transaction.
Mistake 3: Applying the animation to the wrong view
In SwiftUI, the modifier must be attached to the view whose properties change. Putting it on a parent that does not change can make the animation seem broken.
Problem: The animation is attached too high in the hierarchy, so the changing value is not the view actually being animated.
import SwiftUI
struct WrongTarget: View {
@State private var expanded = false
var body: some View {
VStack {
Text("Animated text")
.font(.title)
Button("Toggle") {
expanded.toggle()
}
}
.animation(.spring(), value: expanded)
}
}Fix: Attach the animation to the view whose size, position, opacity, or other animatable properties actually change.
import SwiftUI
struct RightTarget: View {
@State private var expanded = false
var body: some View {
VStack {
Text("Animated text")
.font(.title)
.scaleEffect(expanded ? 1.2 : 1.0)
.animation(.spring(), value: expanded)
Button("Toggle") {
expanded.toggle()
}
}
}
}The corrected version works because the animated modifier is attached to the changing view itself.
7. Best Practices
Use animation to clarify state changes, not decorate everything
Animations should explain what changed. If every tap triggers motion, the interface becomes noisy and harder to use.
// Less preferred: motion on every minor state change
withAnimation {
count += 1
}// Preferred: animate only when the change has visual meaning
if showDetails {
withAnimation {
showDetails = true
}
}This keeps motion purposeful and easier for users to interpret.
Prefer transitions for insert/remove, not for simple property tweaks
Transitions are best when a view appears or disappears. For size, color, or position changes on an existing view, a standard animation is usually the clearer choice.
// Preferred for showing and hiding a panel
if showPanel {
PanelView()
.transition(.move(edge: .bottom))
}This makes your intent easier to read and avoids forcing a transition onto the wrong kind of change.
Keep durations short for interface feedback
Short animations feel responsive. Long ones can make the app feel slow, especially for taps and small state changes.
// Preferred: quick feedback
withAnimation(.easeInOut(duration: 0.2)) {
isOpen.toggle()
}Short animations are especially important for controls, menus, and confirmation states.
8. Limitations and Edge Cases
- Not every property is smoothly animatable. SwiftUI animates many numeric and geometric values, but some changes happen instantly.
- Transitions only work when the view is inserted or removed from the hierarchy, usually through conditional rendering.
- List row animations can behave differently depending on how the data source changes and whether the row identity is stable.
- Some layout changes are animated by SwiftUI, but if the hierarchy is rebuilt in a different way, the motion may look jumpy or inconsistent.
- Using unstable identifiers in a list can make rows appear to animate incorrectly because SwiftUI thinks items are new views.
- If an animation seems to do nothing, the problem is often that the state change is not connected to the animating view or the modifier is on the wrong node.
One common surprise is that animations in SwiftUI are driven by value changes, not by manually telling a view to play a timeline. If the value does not actually change, there is nothing to animate.
9. Practical Mini Project
Let’s build a small expandable info card. It will animate its height, show a description with a transition, and use a spring motion for a more natural feel.
import SwiftUI
struct InfoCardView: View {
@State private var isExpanded = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Project Status")
.font(.headline)
Spacer()
Button(isExpanded ? "Hide" : "Show") {
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
isExpanded.toggle()
}
}
}
Text("The current build passed all checks and is ready for review.")
.font(.subheadline)
.foregroundColor(.secondary)
if isExpanded {
Text("This card expands to reveal more detailed information. The extra text appears with a transition so the change feels smooth.")
.transition(.move(edge: .top).combined(with: .opacity))
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(RoundedRectangle(cornerRadius: 16).fill(Color.blue.opacity(0.1)))
.padding()
}
}This mini project combines the key ideas: state controls the UI, the button changes the state inside withAnimation, and the conditional text uses a transition when it appears or disappears.
10. Key Points
- SwiftUI animations are usually driven by state changes.
- withAnimation creates an explicit animated update.
- .animation(..., value:) ties animation to a specific state value.
- .transition controls how a view enters or leaves the hierarchy.
- Transitions need conditional insertion or removal to work.
- Good animations explain changes instead of distracting from them.
11. Practice Exercise
- Create a SwiftUI view with a button and a hidden message.
- When the button is tapped, the message should appear with a slide-and-fade transition.
- Tap the button again to remove the message with the same animation.
- Make the container slightly resize so you can see the layout adjust smoothly.
Expected output: A button toggles a short message that slides in, fades in, and then slides out and fades out when hidden.
Hint: Put the message inside an if statement, attach a transition to it, and wrap the toggle in withAnimation.
import SwiftUI
struct ExerciseSolution: View {
@State private var showHint = false
var body: some View {
VStack(alignment: .leading, spacing: 14) {
Button("Toggle Hint") {
withAnimation(.easeInOut(duration: 0.25)) {
showHint.toggle()
}
}
if showHint {
Text("Use state, conditional rendering, and a transition.")
.padding()
.background(Color.gray.opacity(0.15))
.cornerRadius(10)
.transition(.move(edge: .top).combined(with: .opacity))
}
}
.padding()
}
}This solution demonstrates the full pattern you will use most often in SwiftUI: toggle state, render conditionally, and let the animation system handle the motion.
12. Final Summary
SwiftUI animations and transitions are built around state. When values change, SwiftUI can interpolate view properties smoothly, and when views are inserted or removed, transitions describe how that motion should look. That makes animation a natural extension of how SwiftUI already updates the interface.
The main skills to remember are when to use withAnimation, when to use .animation(..., value:), and when a transition needs a conditional if to work. Once those pieces are clear, you can build interfaces that feel responsive and easy to understand.
Next, try combining animations with matchedGeometryEffect or list updates so you can see how SwiftUI handles motion across more complex layouts.