Skip to content

SwiftUI Animations — Implicit, Explicit, Transitions, and Matched Geometry

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about SwiftUI Animations. We cover key concepts, practical examples, and best practices to help you master this topic.

SwiftUI animations provide declarative, fluid motion between view states using implicit animations attached to views, explicit animations triggered by state changes, and transitions that control how views appear and disappear.

What You'll Learn

  • Implicit animations with .animation() modifier
  • Explicit animations with withAnimation
  • View transitions (opacity, scale, slide, custom)
  • MatchedGeometryEffect for seamless motion between views
  • Custom timing curves and spring animations
  • Performance best practices

Why It Matters

Animations guide user attention, provide feedback, and make apps feel polished and professional. A button that smoothly scales when pressed, a list that animates row deletions, or a card that expands to a detail view — these micro-interactions significantly improve perceived quality.

Real-World Use

A weather app uses matchedGeometryEffect to animate a city card expanding into a full-screen forecast view, spring animations for the temperature number updating, opacity transitions for loading states, and implicit animation for the sunrise and sunset progress bar smoothly filling throughout the day.

Learning Path

flowchart LR
  A[SwiftUI Lists
Lesson 28] --> B[SwiftUI Animations
You are here] B --> C[SwiftUI Networking
Lesson 30] B --> D[Testing
Lesson 31] style B fill:#f90,color:#fff

Implicit Animations

Attach .animation() to a view to automatically animate any animatable changes.

import SwiftUI

struct ImplicitAnimationView: View {
    @State private var scale = 1.0
    @State private var rotation = 0.0
    @State private var offset = CGSize.zero
    @State private var color = Color.blue

    var body: some View {
        VStack(spacing: 20) {
            RoundedRectangle(cornerRadius: 20)
                .fill(color)
                .frame(width: 150, height: 150)
                .scaleEffect(scale)
                .rotationEffect(.degrees(rotation))
                .offset(offset)
                .animation(.spring(response: 0.5, dampingFraction: 0.6), value: scale)
                .animation(.easeInOut(duration: 0.5), value: rotation)
                .animation(.interpolatingSpring(stiffness: 100, damping: 10), value: offset)
                .animation(.default, value: color)

            HStack(spacing: 12) {
                Button("Scale") { scale = scale == 1.0 ? 1.5 : 1.0 }
                Button("Rotate") { rotation += 45 }
                Button("Move") { offset = offset == .zero ? CGSize(width: 50, height: 50) : .zero }
                Button("Color") { color = color == .blue ? .green : .blue }
            }
            .buttonStyle(.bordered)
        }
    }
}

Each animatable property gets its own animation. The value parameter tells SwiftUI which state change triggers the animation.

Explicit Animations

Use withAnimation to animate changes that result from state modifications.

import SwiftUI

struct CardData: Identifiable {
    let id = UUID()
    let title: String
    let color: Color
    var isSelected = false
}

struct ExplicitAnimationView: View {
    @State private var isExpanded = false
    @State private var cards = [
        CardData(title: "Card 1", color: .red),
        CardData(title: "Card 2", color: .blue),
        CardData(title: "Card 3", color: .green)
    ]

    var body: some View {
        VStack(spacing: 16) {
            Toggle("Expand Cards", isOn: $isExpanded)
                .padding(.horizontal)

            ForEach($cards) { $card in
                CardView(card: card, isExpanded: isExpanded)
                    .onTapGesture {
                        withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
                            card.isSelected.toggle()
                        }
                    }
            }

            Button("Shuffle Order") {
                withAnimation(.interactiveSpring()) {
                    cards.shuffle()
                }
            }
            .buttonStyle(.bordered)
        }
        .padding()
    }
}

struct CardView: View {
    let card: CardData
    let isExpanded: Bool

    var body: some View {
        RoundedRectangle(cornerRadius: 12)
            .fill(card.color.opacity(card.isSelected ? 1.0 : 0.6))
            .frame(height: isExpanded ? 120 : 60)
            .overlay {
                HStack {
                    Text(card.title)
                        .foregroundColor(.white)
                        .fontWeight(.bold)
                    if card.isSelected {
                        Image(systemName: "checkmark.circle.fill")
                            .foregroundColor(.white)
                    }
                }
            }
    }
}

View Transitions

Transitions control how views appear and disappear. Combine with if statements and withAnimation.

import SwiftUI

struct ScaledAndOffset: ViewModifier {
    let scale: CGFloat
    let offset: CGFloat

    func body(content: Content) -> some View {
        content
            .scaleEffect(scale)
            .offset(y: offset)
    }
}

struct TransitionExampleView: View {
    @State private var showDetail = false

    var body: some View {
        VStack(spacing: 16) {
            Button(showDetail ? "Hide Detail" : "Show Detail") {
                withAnimation(.spring()) {
                    showDetail.toggle()
                }
            }
            .buttonStyle(.borderedProminent)

            if showDetail {
                VStack(spacing: 12) {
                    Image(systemName: "star.fill")
                        .font(.largeTitle)
                        .foregroundColor(.yellow)
                    Text("Detail Content")
                        .font(.title2)
                    Text("This content appears with a smooth transition.")
                        .foregroundColor(.secondary)
                }
                .padding()
                .background(RoundedRectangle(cornerRadius: 16).fill(Color.blue.opacity(0.1)))
                .padding()
                .transition(.asymmetric(
                    insertion: .scale(scale: 0.3).combined(with: .opacity),
                    removal: .slide.combined(with: .opacity)
                ))
            }

            Spacer()
        }
        .padding()
    }
}

Transitions apply only to views entering or leaving the view hierarchy. Use .asymmetric for different insertion and removal effects.

MatchedGeometryEffect

Create seamless animations where a view appears to transform into another view.

import SwiftUI

struct MatchedGeometryExample: View {
    @Namespace private var animation
    @State private var isExpanded = false
    @State private var selectedItem: String?

    let items = ["Photos", "Music", "Videos", "Documents"]

    var body: some View {
        VStack {
            if isExpanded, let item = selectedItem {
                ExpandedView(
                    item: item,
                    namespace: animation,
                    onClose: {
                        withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                            isExpanded = false
                            selectedItem = nil
                        }
                    }
                )
            } else {
                LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
                    ForEach(items, id: \.self) { item in
                        RoundedRectangle(cornerRadius: 16)
                            .fill(Color.blue.opacity(0.2))
                            .frame(height: 120)
                            .overlay(Text(item).fontWeight(.bold))
                            .matchedGeometryEffect(id: item, in: animation)
                            .onTapGesture {
                                withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                                    selectedItem = item
                                    isExpanded = true
                                }
                            }
                    }
                }
                .padding()
            }
        }
    }
}

struct ExpandedView: View {
    let item: String
    let namespace: Namespace.ID
    let onClose: () -> Void

    var body: some View {
        VStack(spacing: 20) {
            RoundedRectangle(cornerRadius: 16)
                .fill(Color.blue.opacity(0.2))
                .frame(height: 300)
                .overlay {
                    VStack {
                        Text(item)
                            .font(.largeTitle)
                            .fontWeight(.bold)
                        Button("Close") { onClose() }
                            .buttonStyle(.bordered)
                    }
                }
                .matchedGeometryEffect(id: item, in: namespace)

            Text("Detailed view for \(item)")
                .font(.body)
        }
        .padding()
    }
}

The geometry effect links views across the hierarchy with the same identifier. When state changes, SwiftUI animates the size, position, and shape.

Custom Timing Curves

SwiftUI provides several built-in animation types and supports custom curves.

import SwiftUI

struct TimingCurvesView: View {
    @State private var offset: CGFloat = -100
    @State private var isAnimating = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Animate") {
                offset = isAnimating ? -100 : 100
                isAnimating.toggle()
            }
            .buttonStyle(.borderedProminent)

            VStack(alignment: .leading, spacing: 8) {
                Text("Spring").font(.caption)
                Circle().fill(.red).frame(width: 20)
                    .offset(x: offset)
                    .animation(.spring(response: 0.5, dampingFraction: 0.5), value: offset)

                Text("Ease In Out").font(.caption)
                Circle().fill(.green).frame(width: 20)
                    .offset(x: offset)
                    .animation(.easeInOut(duration: 0.8), value: offset)

                Text("Interpolating Spring").font(.caption)
                Circle().fill(.blue).frame(width: 20)
                    .offset(x: offset)
                    .animation(.interpolatingSpring(stiffness: 100, damping: 5), value: offset)

                Text("Bouncy").font(.caption)
                Circle().fill(.orange).frame(width: 20)
                    .offset(x: offset)
                    .animation(.bouncy(duration: 0.6, extraBounce: 0.3), value: offset)

                Text("Custom Curve").font(.caption)
                Circle().fill(.purple).frame(width: 20)
                    .offset(x: offset)
                    .animation(.timingCurve(0.2, 0.8, 0.8, 0.2, duration: 0.6), value: offset)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
        }
    }
}

Performance Best Practices

import SwiftUI

struct AnimationPerformanceView: View {
    @State private var items = Array(0..<50)

    var body: some View {
        VStack {
            // GOOD: Only animatable properties change
            Circle()
                .fill(.blue)
                .frame(width: 50)
                .scaleEffect(items.isEmpty ? 0.5 : 1.0)
                .animation(.default, value: items.isEmpty)

            // GOOD: Using .drawingGroup for complex layer effects
            ZStack {
                ForEach(0..<20, id: \.self) { i in
                    Circle()
                        .fill(Color(hue: Double(i) / 20, saturation: 0.5, brightness: 1))
                        .frame(width: 30)
                        .offset(x: CGFloat(i * 5), y: CGFloat(i * 3))
                        .opacity(0.5)
                }
            }
            .drawingGroup() // Offscreen render for performance
        }
    }
}

Only animate properties that are lightweight (position, scale, opacity, rotation). Avoid animating properties that trigger layout passes (frame, padding) when possible.

Common Mistakes

  1. Animating too many properties simultaneously: Each animated property adds GPU work. Limit concurrent animations to maintain 60fps.

  2. Using implicit animations on container views: Attaching .animation() to a List or VStack may unintentionally animate all children. Use explicit withAnimation instead.

  3. Forgetting the value parameter: Without the value parameter, .animation() animates all property changes, including initial setup. Always specify which value triggers the animation.

  4. Overusing matchedGeometryEffect: Each matched geometry effect requires coordinate space computation. Use it sparingly for hero animations only.

  5. Animating non-animatable properties: Only visual properties can animate. You cannot animate data or logic — only the visual representation of state changes.

Practice Questions

  1. What is the difference between implicit and explicit animations?
  2. How do transitions differ from animations?
  3. What problem does matchedGeometryEffect solve?
  4. Why should you specify the value parameter in .animation()?
  5. Challenge: Build a card deck where tapping a card flips it (rotation3DEffect), then expands it to full screen (matchedGeometryEffect), and tapping the expanded view shrinks it back. The flip and expand should use different animation curves.

Mini Project

Create an AnimatedOnboarding with:

  • Three onboarding screens that transition with asymmetric transitions (slide in from right, slide out to left)
  • A page indicator that animates between dots (scale and color)
  • A "Get Started" button that has a spring animation on appear
  • An icon that uses matchedGeometryEffect to move from the last screen to the main app view
  • Smooth opacity and blur transitions between all states

FAQ

Can I animate between different view types?

Yes. Use AnyTransition with custom modifiers or matchedGeometryEffect to animate between completely different view hierarchies.

What is the difference between .animation and .transition?

Animation affects continuous property changes (position, scale, color). Transition affects view insertion and removal from the hierarchy.

How do I stop an animation mid-way?

Remove the animation modifier or set the animated property to its final value outside of withAnimation.

What are transaction animations?

Transactions let you override animations for a specific state change using withTransaction(:) instead of withAnimation(:).

Can I animate the appearance of a view modifier?

Yes. Create a custom ViewModifier that conforms to Animatable and implement the animatableData property.

What's Next

Combine animations with real data in SwiftUI Networking, or learn testing patterns in Testing.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro