Skip to content
intermediate Phase 7 · Advanced UI

Animations & Transitions

Implement implicit/explicit animations, matchedGeometryEffect, transitions, and spring physics.

55m
3 problems
Topic Progress 0%

Implicit Animations

What Are Implicit Animations?

Implicit animations automatically animate when the animatable properties of a view change. You attach the .animation() modifier to a view, and SwiftUI handles the transition smoothly whenever state changes.

The .animation Modifier

The simplest way to add animation is by attaching .animation() to any view. When any animatable property changes, the animation plays automatically.

struct PulseView: View {
    @State private var isScaled = false
    
    var body: some View {
        Circle()
            .fill(Color.blue)
            .frame(width: 100, height: 100)
            .scaleEffect(isScaled ? 1.5 : 1.0)
            .animation(
                .easeInOut(duration: 0.8)
                .repeatForever(autoreverses: true),
                value: isScaled
            )
            .onAppear {
                isScaled = true
            }
    }
}

Animation Curves

SwiftUI provides several built-in timing curves that control the acceleration and deceleration of animations:

  • .linear - Constant speed throughout
  • .easeIn - Starts slow, speeds up
  • .easeOut - Starts fast, slows down
  • .easeInOut - Slow start and end, fast middle
  • .spring(response:dampingFraction:blendDuration:) - Physics-based spring animation
struct AnimationCurvesDemo: View {
    @State private var offset: CGFloat = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Capsule()
                .fill(.red)
                .frame(width: 80, height: 40)
                .offset(x: offset)
                .animation(.linear(duration: 1), value: offset)
            
            Capsule()
                .fill(.green)
                .frame(width: 80, height: 40)
                .offset(x: offset)
                .animation(.easeIn(duration: 1), value: offset)
            
            Capsule()
                .fill(.blue)
                .frame(width: 80, height: 40)
                .offset(x: offset)
                .animation(.spring(response: 0.5, dampingFraction: 0.6), value: offset)
        }
        .onTapGesture {
            offset = offset == 0 ? 150 : 0
        }
    }
}

Animatable Properties

Not all properties are animatable. SwiftUI animates changes to properties like frame, offset, opacity, rotation, scale, and foregroundColor. Custom properties can be animated using the Animatable protocol.

Conditional Animation

You can make animations apply only to specific properties or use the .animation(_:value:) syntax to trigger animation only when a specific value changes.

struct ConditionalAnimation: View {
    @State private var isActive = false
    @State private var color: Color = .blue
    
    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 12)
                .fill(color)
                .frame(
                    width: isActive ? 200 : 100,
                    height: isActive ? 200 : 100
                )
                .animation(.spring(), value: isActive)
                .animation(.easeInOut(duration: 1), value: color)
            
            Button("Toggle Size") {
                isActive.toggle()
            }
            Button("Change Color") {
                color = color == .blue ? .purple : .blue
            }
        }
    }
}

Explicit Animations

The withAnimation Block

Explicit animations give you precise control over what animates and when. The withAnimation closure wraps state changes, and SwiftUI animates all view changes that result from those state changes.

struct ExplicitAnimationDemo: View {
    @State private var isRotated = false
    @State private var isScaled = false
    @State private var opacity: Double = 1
    
    var body: some View {
        VStack(spacing: 30) {
            Rectangle()
                .fill(.blue)
                .frame(width: 100, height: 100)
                .rotationEffect(.degrees(isRotated ? 45 : 0))
                .scaleEffect(isScaled ? 1.5 : 1.0)
                .opacity(opacity)
            
            HStack {
                Button("Rotate") {
                    withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                        isRotated.toggle()
                    }
                }
                
                Button("Scale") {
                    withAnimation(.easeInOut(duration: 0.5)) {
                        isScaled.toggle()
                    }
                }
                
                Button("Fade") {
                    withAnimation(.linear(duration: 1)) {
                        opacity = opacity == 1 ? 0.3 : 1
                    }
                }
            }
        }
    }
}

Controlling Animation Speed

You can customize animation duration and behavior by passing different animation types to withAnimation:

// Fast spring
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
    stateChange()
}

// Slow ease
withAnimation(.easeInOut(duration: 2.0)) {
    stateChange()
}

// Repeating animation
withAnimation(.linear(duration: 1).repeatCount(3, autoreverses: true)) {
    stateChange()
}

// Delayed animation
withAnimation(.easeIn(duration: 0.5).delay(0.2)) {
    stateChange()
}

Animating Multiple Properties

When using withAnimation, all animatable properties that change within the block animate simultaneously. You can stagger animations by using nested withAnimation calls or by using the .animation modifier with specific values.

struct StaggeredAnimation: View {
    @State private var showItems = false
    let items = ["Item 1", "Item 2", "Item 3", "Item 4"]
    
    var body: some View {
        VStack {
            ForEach(Array(items.enumerated()), id: \ .offset) { index, item in
                Text(item)
                    .padding()
                    .background(Color.blue.opacity(0.2))
                    .cornerRadius(8)
                    .offset(x: showItems ? 0 : -200)
                    .opacity(showItems ? 1 : 0)
                    .animation(
                        .spring(response: 0.5, dampingFraction: 0.7)
                            .delay(Double(index) * 0.1),
                        value: showItems
                    )
            }
            
            Button("Show Items") {
                showItems.toggle()
            }
        }
        .padding()
    }
}

Transitions & matchedGeometryEffect

View Transitions

Transitions define how a view is inserted into or removed from the view hierarchy. SwiftUI provides built-in transitions and lets you combine them for complex effects.

struct TransitionDemo: View {
    @State private var showDetail = false
    
    var body: some View {
        VStack {
            if showDetail {
                Text("Detail View")
                    .font(.title)
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(12)
                    .transition(.slide)
            }
            
            Button("Toggle") {
                withAnimation {
                    showDetail.toggle()
                }
            }
        }
    }
}

Built-in Transitions

SwiftUI includes several built-in transitions:

  • .opacity - Fades in and out
  • .slide - Slides in from leading edge
  • .move(edge:) - Moves in from specified edge
  • .scale - Scales from zero to full size
  • .offset(x:y:) - Offsets from specified position
// Combining transitions
.transition(.asymmetric(
    insertion: .slide.combined(with: .opacity),
    removal: .scale.combined(with: .opacity)
))

// Using modifiers with transitions
.transition(.offset(x: 0, y: 100).combined(with: .opacity))

matchedGeometryEffect

matchedGeometryEffect creates seamless hero animations between views. It links the geometry of one view to another, allowing smooth transitions when views appear, disappear, or move.

struct MatchedGeometryDemo: View {
    @Namespace private var animationNamespace
    @State private var isExpanded = false
    
    var body: some View {
        VStack {
            if isExpanded {
                // Expanded view
                RoundedRectangle(cornerRadius: 20)
                    .fill(
                        LinearGradient(
                            colors: [.blue, .purple],
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        )
                    )
                    .frame(width: 350, height: 500)
                    .overlay(
                        VStack {
                            Text("Expanded Card")
                                .font(.title)
                                .foregroundColor(.white)
                            Text("Tap to collapse")
                                .foregroundColor(.white.opacity(0.8))
                        }
                    )
                    .matchedGeometryEffect(id: "card", in: animationNamespace)
            } else {
                // Collapsed view
                RoundedRectangle(cornerRadius: 12)
                    .fill(.blue)
                    .frame(width: 100, height: 80)
                    .overlay(
                        Text("Card")
                            .foregroundColor(.white)
                    )
                    .matchedGeometryEffect(id: "card", in: animationNamespace)
            }
            
            Button("Toggle") {
                withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                    isExpanded.toggle()
                }
            }
            .padding(.top, 40)
        }
    }
}

Namespace and Identifiers

Each matchedGeometryEffect requires a unique ID within a Namespace. The ID can be any Hashable value. Ensure that the same ID is used for the source and destination views to create the matching effect.

Practical Use Cases

  • Navigation bar title to large title transitions
  • List item to detail view hero animations
  • Tab bar icon to full-screen transitions
  • Card expand/collapse animations in feeds
  • Image gallery thumbnail to full-screen preview

Quiz

1. What is the difference between implicit and explicit animations in SwiftUI?

Question 1 options

2. Which parameter controls the bounciness of a spring animation?

Question 2 options

3. What does matchedGeometryEffect require to link two views?

Question 3 options

4. How do you combine multiple transitions?

Question 4 options

Flashcards

Question

What is the difference between .animation() modifier and withAnimation {}?

Answer

.animation() is implicit - it animates any property changes on the view. withAnimation {} is explicit - it wraps specific state changes to trigger animation.

Question

What does dampingFraction control in spring animations?

Answer

It controls how much the spring oscillates before settling. Lower values create more bounce, higher values dampen the oscillation.

Question

What is matchedGeometryEffect used for?

Answer

Creating smooth hero animations by linking the geometry of two views, allowing seamless transitions when views appear, disappear, or change position.

Question

Name three built-in SwiftUI transitions.

Answer

.opacity (fade), .slide (slide from edge), .move(edge:) (move from specific edge), .scale (scale from zero), .offset (offset from position).

Revision Notes

Key Takeaways

  • 1. Implicit animations use .animation() modifier, explicit use withAnimation {}
  • 2. Spring animations provide natural, physics-based motion
  • 3. Transitions control how views enter and leave the view hierarchy
  • 4. matchedGeometryEffect creates hero animations between views
  • 5. Combine transitions for complex insertion and removal effects

Interview Tips

  • Explain when to use implicit vs explicit animations
  • Describe how to create a spring animation and what parameters control it
  • Discuss practical uses of matchedGeometryEffect in navigation flows

Cheat Sheet

Animations & Transitions Quick Reference

  • Implicit: view.animation(.spring(), value: triggerValue)
  • Explicit: withAnimation(.spring()) { stateChange() }
  • Spring: .spring(response: 0.5, dampingFraction: 0.7)
  • Transitions: .transition(.slide), .transition(.opacity), .transition(.scale)
  • Combined: .transition(.slide.combined(with: .opacity))
  • matchedGeometryEffect: @Namespace + .matchedGeometryEffect(id:in:)
  • Asymmetric: .transition(.asymmetric(insertion:removal:))