DEV Community

SwiftUI Animation Guide: From Basic to Advanced in 2026

Animations are what separate a good iOS app from a great one. SwiftUI has evolved dramatically since its introduction, and in 2026 it offers one of the most powerful and expressive animation systems on any platform. Whether you are just getting started or want to master advanced techniques, this guide covers everything you need to know.

1. Implicit Animations with .animation()

Implicit animations are the easiest way to add motion to your SwiftUI views. You attach the .animation() modifier, and SwiftUI automatically animates any changes to animatable properties.

struct ImplicitAnimationDemo: View {
    @State private var scale: CGFloat = 1.0
    @State private var opacity: Double = 1.0

    var body: some View {
        VStack(spacing: 30) {
            Circle()
                .fill(.blue.gradient)
                .frame(width: 100, height: 100)
                .scaleEffect(scale)
                .opacity(opacity)
                .animation(.easeInOut(duration: 0.5), value: scale)

            Button("Animate") {
                scale = scale == 1.0 ? 1.5 : 1.0
                opacity = opacity == 1.0 ? 0.5 : 1.0
            }
            .buttonStyle(.borderedProminent)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The key rule: always pass a value parameter to .animation(). This tells SwiftUI exactly which state change should trigger the animation. Without it, you get unpredictable behavior and a deprecation warning.

2. Explicit Animations with withAnimation

When you need more control over what gets animated, use withAnimation {}. This wraps a state change in an animation context, giving you precise control.

struct ExplicitAnimationDemo: View {
    @State private var isExpanded = false
    @State private var rotation: Double = 0

    var body: some View {
        VStack(spacing: 20) {
            RoundedRectangle(cornerRadius: isExpanded ? 20 : 50)
                .fill(.purple.gradient)
                .frame(
                    width: isExpanded ? 300 : 100,
                    height: isExpanded ? 200 : 100
                )
                .rotationEffect(.degrees(rotation))

            Button("Toggle") {
                withAnimation(.easeInOut(duration: 0.6)) {
                    isExpanded.toggle()
                }
                // This change is NOT animated
                rotation += 45
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Use withAnimation when you want some state changes animated and others not. Only the state mutations inside the closure get animated. Here the shape expands smoothly but the rotation snaps instantly.

3. Spring Animations (iOS 17+)

Spring animations create natural, physics-based motion. iOS 17 introduced simplified spring presets that make it trivial to add bouncy, organic movement.

struct SpringAnimationDemo: View {
    @State private var offset: CGFloat = 0

    var body: some View {
        VStack(spacing: 40) {
            // Bouncy spring -- high bounce, fun feel
            Circle()
                .fill(.orange)
                .frame(width: 60, height: 60)
                .offset(y: offset)
                .animation(.bouncy(duration: 0.6, extraBounce: 0.3), value: offset)

            // Snappy spring -- quick and responsive
            Circle()
                .fill(.green)
                .frame(width: 60, height: 60)
                .offset(y: offset)
                .animation(.snappy(duration: 0.4), value: offset)

            // Custom spring with full control
            Circle()
                .fill(.blue)
                .frame(width: 60, height: 60)
                .offset(y: offset)
                .animation(
                    .spring(duration: 0.8, bounce: 0.5, blendDuration: 0.2),
                    value: offset
                )

            Button("Drop") {
                offset = offset == 0 ? 150 : 0
            }
            .buttonStyle(.borderedProminent)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The three main spring presets: .bouncy for playful interfaces, .snappy for responsive UI interactions, and .spring() with custom parameters for full control. Spring animations feel more natural than ease curves because they simulate real-world physics.

4. Transition Animations

Transitions control how views appear and disappear. They work with if statements and other conditional rendering in SwiftUI.

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

    var body: some View {
        VStack(spacing: 20) {
            Button("Toggle Detail") {
                withAnimation(.spring(duration: 0.5, bounce: 0.3)) {
                    showDetail.toggle()
                }
            }

            if showDetail {
                DetailCard()
                    .transition(
                        .asymmetric(
                            insertion: .move(edge: .trailing)
                                .combined(with: .opacity),
                            removal: .move(edge: .leading)
                                .combined(with: .opacity)
                        )
                    )
            }
        }
    }
}

struct DetailCard: View {
    var body: some View {
        RoundedRectangle(cornerRadius: 16)
            .fill(.teal.gradient)
            .frame(height: 200)
            .overlay(
                Text("Detail View")
                    .font(.title2.bold())
                    .foregroundColor(.white)
            )
            .padding()
    }
}
Enter fullscreen mode Exit fullscreen mode

The .asymmetric transition lets you define different animations for insertion and removal. You can combine multiple transitions with .combined(with:) for richer effects -- for example, sliding in from the right while fading in.

5. Keyframe Animations (iOS 17+)

KeyframeAnimator lets you define complex, multi-property animations with precise timing control. Think of it as CSS keyframes but for SwiftUI.

struct KeyframeDemo: View {
    @State private var animating = false

    var body: some View {
        VStack {
            Text("Hello!")
                .font(.largeTitle.bold())
                .keyframeAnimator(
                    initialValue: AnimationValues(),
                    trigger: animating
                ) { content, value in
                    content
                        .scaleEffect(value.scale)
                        .rotationEffect(.degrees(value.rotation))
                        .offset(y: value.yOffset)
                } keyframes: { _ in
                    KeyframeTrack(\.scale) {
                        SpringKeyframe(1.5, duration: 0.3)
                        SpringKeyframe(0.8, duration: 0.2)
                        SpringKeyframe(1.0, duration: 0.3)
                    }
                    KeyframeTrack(\.rotation) {
                        LinearKeyframe(0, duration: 0.1)
                        SpringKeyframe(15, duration: 0.15)
                        SpringKeyframe(-15, duration: 0.15)
                        SpringKeyframe(0, duration: 0.2)
                    }
                    KeyframeTrack(\.yOffset) {
                        SpringKeyframe(-30, duration: 0.3)
                        SpringKeyframe(0, duration: 0.4)
                    }
                }

            Button("Animate") { animating.toggle() }
                .buttonStyle(.borderedProminent)
                .padding(.top, 40)
        }
    }
}

struct AnimationValues {
    var scale: CGFloat = 1.0
    var rotation: Double = 0
    var yOffset: CGFloat = 0
}
Enter fullscreen mode Exit fullscreen mode

KeyframeAnimator lets you animate multiple properties independently with different timings and curves. Each KeyframeTrack controls one property, and you can mix spring, linear, and cubic keyframes within the same animation.

6. Phase Animations

PhaseAnimator cycles through a sequence of states automatically, creating repeating multi-step animations without manual state management.

enum BouncePhase: CaseIterable {
    case initial, moveUp, moveDown, settle

    var yOffset: CGFloat {
        switch self {
        case .initial: return 0
        case .moveUp: return -60
        case .moveDown: return 20
        case .settle: return 0
        }
    }

    var scale: CGFloat {
        switch self {
        case .initial: return 1.0
        case .moveUp: return 1.2
        case .moveDown: return 0.9
        case .settle: return 1.0
        }
    }
}

struct PhaseAnimationDemo: View {
    @State private var trigger = false

    var body: some View {
        VStack {
            PhaseAnimator(BouncePhase.allCases, trigger: trigger) { phase in
                Image(systemName: "heart.fill")
                    .font(.system(size: 60))
                    .foregroundStyle(.red.gradient)
                    .offset(y: phase.yOffset)
                    .scaleEffect(phase.scale)
            } animation: { phase in
                switch phase {
                case .initial: .spring(duration: 0.3)
                case .moveUp: .spring(duration: 0.25, bounce: 0.4)
                case .moveDown: .spring(duration: 0.2)
                case .settle: .spring(duration: 0.35)
                }
            }

            Button("Bounce") { trigger.toggle() }
                .buttonStyle(.borderedProminent)
                .padding(.top, 40)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

PhaseAnimator is perfect for loading indicators, attention-grabbing effects, or any animation that goes through distinct stages. Each phase gets its own animation curve, giving you fine-grained control.

7. matchedGeometryEffect for Hero Transitions

matchedGeometryEffect creates smooth hero-style transitions between views by linking elements across different layout states.

struct HeroTransitionDemo: View {
    @State private var showDetail = false
    @Namespace private var animation

    var body: some View {
        ZStack {
            if !showDetail {
                // Thumbnail state
                VStack {
                    RoundedRectangle(cornerRadius: 16)
                        .fill(.indigo.gradient)
                        .matchedGeometryEffect(id: "card", in: animation)
                        .frame(width: 150, height: 150)

                    Text("SwiftUI")
                        .font(.headline)
                        .matchedGeometryEffect(id: "title", in: animation)
                }
                .onTapGesture {
                    withAnimation(.spring(duration: 0.5, bounce: 0.3)) {
                        showDetail = true
                    }
                }
            } else {
                // Expanded state
                VStack(alignment: .leading, spacing: 16) {
                    RoundedRectangle(cornerRadius: 24)
                        .fill(.indigo.gradient)
                        .matchedGeometryEffect(id: "card", in: animation)
                        .frame(height: 300)

                    Text("SwiftUI")
                        .font(.largeTitle.bold())
                        .matchedGeometryEffect(id: "title", in: animation)

                    Text("SwiftUI is Apple's declarative framework for building user interfaces across all Apple platforms.")
                        .font(.body)
                        .foregroundColor(.secondary)
                }
                .padding()
                .onTapGesture {
                    withAnimation(.spring(duration: 0.5, bounce: 0.3)) {
                        showDetail = false
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The @Namespace property wrapper creates a shared coordinate space. Elements with the same id and namespace smoothly interpolate between their two positions and sizes. This is how apps like the App Store create those beautiful expanding card transitions.

8. Performance Tips

Animations can hurt performance if you are not careful. Here are the key optimizations.

struct PerformanceView: View {
    @State private var phase: CGFloat = 0

    var body: some View {
        // Tip 1: Use drawingGroup() for complex view hierarchies
        // Flattens the view into a single Metal layer
        ComplexGradientView()
            .drawingGroup()

        // Tip 2: Prefer Animation over Transaction for simple cases
        // Good -- declarative and clear
        Circle()
            .animation(.spring, value: phase)

        // Tip 3: Use .geometryGroup() in iOS 17+ 
        // to isolate animation effects in subviews
        OuterView()
            .geometryGroup()

        // Tip 4: Avoid animating views that trigger layout recalculation
        // Bad -- animating frame causes layout pass every frame
        // Rectangle().frame(width: animatedWidth)

        // Good -- scaleEffect uses GPU, no layout recalculation
        // Rectangle().scaleEffect(x: scaleFactor)
    }
}
Enter fullscreen mode Exit fullscreen mode

Key takeaways:

  • drawingGroup() -- Renders complex view subtrees into a single Metal texture. Use it when you have many overlapping layers, gradients, or blend modes.
  • GPU-friendly properties -- Prefer .scaleEffect, .rotationEffect, .offset, and .opacity over .frame or .padding for animations. The first group uses GPU compositing; the second triggers CPU layout passes.
  • geometryGroup() (iOS 17+) -- Isolates a subtree so that parent animations do not cause child views to recalculate geometry independently.
  • Transaction vs Animation -- Use .animation() modifier for view-level animation. Use withAnimation for precise state-level control. Avoid the old Transaction API unless you need to override animations in a specific subview.

Wrapping Up

SwiftUI's animation system in 2026 is incredibly powerful. Start with implicit animations for simple state changes, graduate to explicit animations when you need control, and reach for KeyframeAnimator or PhaseAnimator when building complex sequences. For view transitions, matchedGeometryEffect creates polished hero animations with minimal code.

The best way to learn is to experiment. Take each example above, paste it into an Xcode project, and tweak the parameters. You will quickly develop an intuition for what works.

Want 38 pre-built SwiftUI components with smooth animations? Check out my UI Components Pack: https://pease163.github.io/digital-products/


What animation techniques do you use most in your SwiftUI projects? Let me know in the comments!

Top comments (0)