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.

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:

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

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

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

11. Practice Exercise

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.