Day 20 – Animations in SwiftUI
T
Tuan Beo

Day 20 – Animations in SwiftUI

Add motion and interactivity to your UI using SwiftUI’s built-in animation capabilities for smooth, engaging transitions.

1) Implicit animations (one-liners)

Attach .animation(_, value:) to a view. It animates whenever the given value changes.

struct PulseButton: View {
    @State private var big = false

    var body: some View {
        Button("Tap") { big.toggle() }
            .padding()
            .background(.blue)
            .foregroundStyle(.white)
            .clipShape(Capsule())
            .scaleEffect(big ? 1.2 : 1.0)
            .animation(.spring(response: 0.3, dampingFraction: 0.6), value: big)
    }
}

2) Explicit animations (wrap changes)

withAnimation animates only the changes inside the closure.

withAnimation(.easeInOut(duration: 0.25)) {
    vm.toggleDone(task)
}

Use explicit animations in your To‑Do app when toggling completion or inserting/deleting rows.


3) Transitions (appear/disappear)

Animate how a view enters or leaves the hierarchy.

struct RevealPanel: View {
    @State private var show = false

    var body: some View {
        VStack(spacing: 16) {
            Button(show ? "Hide" : "Show") {
                withAnimation(.easeInOut) { show.toggle() }
            }
            if show {
                Text("Hello there 👋")
                    .padding()
                    .background(.ultraThickMaterial)
                    .clipShape(RoundedRectangle(cornerRadius: 16))
                    .transition(.move(edge: .bottom).combined(with: .opacity))
            }
        }
        .padding()
    }
}

Common transitions: .opacity, .scale, .slide, or build your own with AnyTransition.


4) Animating your To‑Do row toggle

Add a little pop when a task is completed.

Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
    .imageScale(.large)
    .symbolRenderingMode(.hierarchical)
    .scaleEffect(task.isDone ? 1.2 : 1.0)
    .foregroundStyle(task.isDone ? .green : .secondary)
    .animation(.spring(response: 0.25, dampingFraction: 0.6), value: task.isDone)

And when toggling:

Button {
    withAnimation(.spring(response: 0.25, dampingFraction: 0.7)) {
        vm.toggleDone(task)
    }
} label { /* icon above */ }
.buttonStyle(.plain)

5) Animate list inserts/deletes

SwiftUI animates list mutations if you change the data inside withAnimation.

withAnimation(.easeInOut) {
    vm.add(TaskItem(title: "New animated task"))
}

withAnimation(.easeInOut) {
    vm.delete(at: offsets)
}

6) Matched geometry for smooth re‑layout

Great for chip → detail or grid → list effects.

struct ChipsDemo: View {
    @Namespace private var ns
    @State private var selected: String? = nil
    let tags = ["SwiftUI","Networking","Core Data","Testing"]

    var body: some View {
        VStack(spacing: 20) {
            HStack {
                ForEach(tags, id: \.self) { tag in
                    Text(tag)
                        .padding(.horizontal, 12).padding(.vertical, 8)
                        .background(.blue.opacity(0.15))
                        .clipShape(Capsule())
                        .matchedGeometryEffect(id: tag, in: ns)
                        .onTapGesture {
                            withAnimation(.spring()) { selected = tag }
                        }
                }
            }

            if let tag = selected {
                VStack(spacing: 12) {
                    Text(tag).font(.headline)
                        .matchedGeometryEffect(id: tag, in: ns)
                    Text("Details about \(tag)…").foregroundStyle(.secondary)
                    Button("Close") {
                        withAnimation(.spring()) { selected = nil }
                    }
                }
                .padding()
                .frame(maxWidth: .infinity)
                .background(.thinMaterial)
                .clipShape(RoundedRectangle(cornerRadius: 16))
                .transition(.opacity.combined(with: .scale))
            }
        }
        .padding()
    }
}

7) Task completion “confetti” (simple)

Quick celebratory animation using a temporary overlay.

struct DoneConfetti: View {
    @State private var show = false

    var body: some View {
        ZStack {
            Button("Complete Task") {
                withAnimation(.spring()) { show = true }
                // Hide after a moment
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
                    withAnimation(.easeOut) { show = false }
                }
            }

            if show {
                Image(systemName: "checkmark.seal.fill")
                    .font(.system(size: 72))
                    .foregroundStyle(.green)
                    .transition(.scale.combined(with: .opacity))
                    .zIndex(1)
            }
        }
    }
}

Hook this into your To‑Do toggle: when a task becomes done, flip a @State showCelebration for that row.


8) Animation cookbook (useful presets)

  • Ease: .easeIn, .easeOut, .easeInOut(duration: 0.25)

  • Spring: .spring(response: 0.35, dampingFraction: 0.7) (snappy)

  • Bouncy iOS‑style: .interpolatingSpring(stiffness: 200, damping: 20)

  • Repeat: .repeatForever(autoreverses: true) (remember to start it in .onAppear)

Example shimmer skeleton:

struct Shimmer: View {
    @State private var phase: CGFloat = -0.8
    var body: some View {
        RoundedRectangle(cornerRadius: 12)
            .fill(.gray.opacity(0.25))
            .overlay(
                LinearGradient(stops: [
                    .init(color: .white.opacity(0.0), location: 0.0),
                    .init(color: .white.opacity(0.7), location: 0.5),
                    .init(color: .white.opacity(0.0), location: 1.0)
                ], startPoint: .topLeading, endPoint: .bottomTrailing)
                .offset(x: phase * 200)
            )
            .onAppear {
                withAnimation(.linear(duration: 1.2).repeatForever(autoreverses: false)) {
                    phase = 0.8
                }
            }
    }
}

🛠 Practice (apply to your To‑Do app)

  1. Row toggle pop: Animate checkmark + slight row scale on completion.

  2. Insert transition: When adding a task, slide it in from the top with .transition(.move(edge: .top).combined(with: .opacity)).

  3. Edit sheet: Animate the “New Task” sheet controls with .spring() when fields appear/disappear (e.g., show Due Date picker behind a toggle).

  4. Matched geometry: Animate a selected task title from the list into a detail header.

  5. Empty state: Animate ContentUnavailableView fade in/out as the list becomes empty/non‑empty.

Comments