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

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

Common Errors (and the fix)

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!

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

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.