iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Animations

SwiftUI animations are a single API surface: you change state, you wrap the change in `withAnimation`, the framework interpolates. Pair with `.matchedGeometryEffect` for hero transitions, `.phaseAnimator` for sequenced animations, and explicit transitions for enter/exit.

withAnimation, transitions, matchedGeometry, phases

EXAMPLE
import SwiftUI

// 1) Implicit animation — same state, declarative transition
struct Counter: View {
    @State private var n = 0
    var body: some View {
        VStack(spacing: 24) {
            Text("\(n)")
                .font(.system(size: 96, weight: .bold))
                .contentTransition(.numericText())  // animate digit changes
                .animation(.bouncy, value: n)

            Button("Increment") {
                withAnimation { n += 1 }            // wrap state changes
            }
        }
        .padding()
    }
}

// 2) Enter / exit transitions
struct ToggleCard: View {
    @State private var show = false
    var body: some View {
        VStack {
            Button(show ? "Hide" : "Show") {
                withAnimation(.spring) { show.toggle() }
            }
            if show {
                Text("Hello")
                    .padding()
                    .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
                    .transition(.scale.combined(with: .opacity))
            }
        }
    }
}

// 3) Hero transition with matchedGeometryEffect
struct HeroDemo: View {
    @Namespace private var ns
    @State private var selected: Int? = nil
    let items = [1, 2, 3]

    var body: some View {
        ZStack {
            if let s = selected {
                VStack {
                    Color.blue
                        .matchedGeometryEffect(id: s, in: ns)
                        .frame(width: 200, height: 200)
                        .onTapGesture { withAnimation(.spring) { selected = nil } }
                    Text("Item \(s)").font(.title)
                }
            } else {
                HStack {
                    ForEach(items, id: \.self) { i in
                        Color.blue
                            .matchedGeometryEffect(id: i, in: ns)
                            .frame(width: 60, height: 60)
                            .onTapGesture { withAnimation(.spring) { selected = i } }
                    }
                }
            }
        }
    }
}

// 4) Phase animator — keyframed without the math
struct PhaseLogo: View {
    var body: some View {
        Image(systemName: "sparkles")
            .font(.largeTitle)
            .phaseAnimator([0, 1]) { content, phase in
                content
                    .scaleEffect(phase == 0 ? 1 : 1.2)
                    .rotationEffect(.degrees(phase == 0 ? 0 : 12))
                    .opacity(phase == 0 ? 1 : 0.8)
            } animation: { phase in
                .easeInOut(duration: 0.6)
            }
    }
}

// 5) Animation curves
//   .linear, .easeIn, .easeOut, .easeInOut, .spring(), .bouncy, .smooth, .snappy
//   .spring(duration: 0.4, bounce: 0.3)
//   custom Animation(.timingCurve(0.2, 0, 0.2, 1, duration: 0.4))

// 6) Animation modifier — declarative trigger
struct Card: View {
    @State private var pressed = false
    var body: some View {
        RoundedRectangle(cornerRadius: 16)
            .fill(.indigo)
            .frame(width: 160, height: 200)
            .scaleEffect(pressed ? 0.98 : 1.0)
            .animation(.easeOut(duration: 0.15), value: pressed)
            .onTapGesture { withAnimation { /* something */ } }
            .onLongPressGesture(minimumDuration: 0.0,
                                pressing: { pressed = $0 },
                                perform: {})
    }
}

// 7) Accessibility — respect Reduce Motion
struct RespectMotion: View {
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var on = false
    var body: some View {
        Image(systemName: on ? "heart.fill" : "heart")
            .scaleEffect(on ? 1.2 : 1.0)
            .animation(reduceMotion ? nil : .bouncy, value: on)
            .onTapGesture { on.toggle() }
    }
}

// 8) Pitfalls
// - Mixing implicit .animation() AND withAnimation() can double-apply
// - Forgetting `value:` on .animation -> animates everything that changes
// - animationdriving on state that resets every frame -> jitter
// - Heavy work inside the animated body -> drops to CPU rendering
// - Skipping reduceMotion -> some users hate motion (and Apple guidelines)

Why it matters

`withAnimation { state = next }` is the right default — declarative, scoped, and composes with every transition modifier. Reach for `phaseAnimator` and `matchedGeometryEffect` when the animation has more than one stage or moves between containers; for simple state-driven motion, the implicit API is enough.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
withAnimation(.easeOut(duration: 0.4)) {
    expanded.toggle()
}
Try it Yourself »

Discussion

Loading…