Back to Provenance

SwiftUI Animation Basics

.agents/skills/swiftui-expert-skill/references/animation-basics.md

3.3.16.7 KB
Original Source

SwiftUI Animation Basics

Core animation concepts, implicit vs explicit animations, timing curves, and performance patterns.

Table of Contents


Core Concepts

State changes trigger view updates. SwiftUI provides mechanisms to animate these changes.

Animation Process:

  1. State change triggers view tree re-evaluation
  2. SwiftUI compares new tree to current render tree
  3. Animatable properties are identified and interpolated (~60 fps)

Key Characteristics:

  • Animations are additive and cancelable
  • Always start from current render tree state
  • Blend smoothly when interrupted

Implicit Animations

Use .animation(_:value:) to animate when a specific value changes.

swift
// GOOD - uses value parameter
Rectangle()
    .frame(width: isExpanded ? 200 : 100, height: 50)
    .animation(.spring, value: isExpanded)
    .onTapGesture { isExpanded.toggle() }

// BAD - deprecated, animates all changes unexpectedly
Rectangle()
    .frame(width: isExpanded ? 200 : 100, height: 50)
    .animation(.spring)  // Deprecated!

Explicit Animations

Use withAnimation for event-driven state changes.

swift
// GOOD - explicit animation
Button("Toggle") {
    withAnimation(.spring) {
        isExpanded.toggle()
    }
}

// BAD - no animation context
Button("Toggle") {
    isExpanded.toggle()  // Abrupt change
}

When to use which:

  • Implicit: Animations tied to specific value changes, precise view tree scope
  • Explicit: Event-driven animations (button taps, gestures)

Animation Placement

Place animation modifiers after the properties they should animate.

swift
// GOOD - animation after properties
Rectangle()
    .frame(width: isExpanded ? 200 : 100, height: 50)
    .foregroundStyle(isExpanded ? .blue : .red)
    .animation(.default, value: isExpanded)  // Animates both

// BAD - animation before properties
Rectangle()
    .animation(.default, value: isExpanded)  // Too early!
    .frame(width: isExpanded ? 200 : 100, height: 50)

Selective Animation

Animate only specific properties using multiple animation modifiers or scoped animations.

swift
// GOOD - selective animation
Rectangle()
    .frame(width: isExpanded ? 200 : 100, height: 50)
    .animation(.spring, value: isExpanded)  // Animate size
    .foregroundStyle(isExpanded ? .blue : .red)
    .animation(nil, value: isExpanded)  // Don't animate color

// iOS 17+ scoped animation
Rectangle()
    .foregroundStyle(isExpanded ? .blue : .red)  // Not animated
    .animation(.spring) {
        $0.frame(width: isExpanded ? 200 : 100, height: 50)  // Animated
    }

Timing Curves

Built-in Curves

CurveUse Case
.springInteractive elements, most UI
.easeInOutAppearance changes
.bouncyPlayful feedback (iOS 17+)
.linearProgress indicators only

Modifiers

swift
.animation(.default.speed(2.0), value: flag)  // 2x faster
.animation(.default.delay(0.5), value: flag)  // Delayed start
.animation(.default.repeatCount(3, autoreverses: true), value: flag)

Good vs Bad Timing

swift
// GOOD - appropriate timing for interaction type
Button("Tap") {
    withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
        isActive.toggle()
    }
}
.scaleEffect(isActive ? 0.95 : 1.0)

// BAD - too slow for button feedback
Button("Tap") {
    withAnimation(.easeInOut(duration: 1.0)) {  // Way too slow!
        isActive.toggle()
    }
}

// BAD - linear feels robotic
Rectangle()
    .animation(.linear(duration: 0.5), value: isActive)  // Mechanical

Animation Performance

Prefer Transforms Over Layout

swift
// GOOD - GPU accelerated transforms
Rectangle()
    .frame(width: 100, height: 100)
    .scaleEffect(isActive ? 1.5 : 1.0)  // Fast
    .offset(x: isActive ? 50 : 0)        // Fast
    .rotationEffect(.degrees(isActive ? 45 : 0))  // Fast
    .animation(.spring, value: isActive)

// BAD - layout changes are expensive
Rectangle()
    .frame(width: isActive ? 150 : 100, height: isActive ? 150 : 100)  // Expensive
    .padding(isActive ? 50 : 0)  // Expensive

Narrow Animation Scope

swift
// GOOD - animation scoped to specific subview
VStack {
    HeaderView()  // Not affected
    ExpandableContent(isExpanded: isExpanded)
        .animation(.spring, value: isExpanded)  // Only this
    FooterView()  // Not affected
}

// BAD - animation at root
VStack {
    HeaderView()
    ExpandableContent(isExpanded: isExpanded)
    FooterView()
}
.animation(.spring, value: isExpanded)  // Animates everything

Avoid Animation in Hot Paths

swift
// GOOD - gate by threshold
.onPreferenceChange(ScrollOffsetKey.self) { offset in
    let shouldShow = offset.y < -50
    if shouldShow != showTitle {  // Only when crossing threshold
        withAnimation(.easeOut(duration: 0.2)) {
            showTitle = shouldShow
        }
    }
}

// BAD - animating every scroll change
.onPreferenceChange(ScrollOffsetKey.self) { offset in
    withAnimation {  // Fires constantly!
        self.offset = offset.y
    }
}

Disabling Animations

swift
// GOOD - disable with transaction
Text("Count: \(count)")
    .transaction { $0.animation = nil }

// GOOD - disable from parent context
DataView()
    .transaction { $0.disablesAnimations = true }

// BAD - hacky zero duration
Text("Count: \(count)")
    .animation(.linear(duration: 0), value: count)  // Hacky

Debugging

swift
// Slow down for inspection
#if DEBUG
.animation(.linear(duration: 3.0).speed(0.2), value: isExpanded)
#else
.animation(.spring, value: isExpanded)
#endif

// Debug modifier to log values
struct AnimationDebugModifier: ViewModifier, Animatable {
    var value: Double
    var animatableData: Double {
        get { value }
        set {
            value = newValue
            print("Animation: \(newValue)")
        }
    }
    func body(content: Content) -> some View {
        content.opacity(value)
    }
}

Quick Reference

Do

  • Use .animation(_:value:) with value parameter
  • Use withAnimation for event-driven animations
  • Prefer transforms over layout changes
  • Scope animations narrowly
  • Choose appropriate timing curves

Don't

  • Use deprecated .animation(_:) without value
  • Animate layout properties in hot paths
  • Apply broad animations at root level
  • Use linear timing for UI (feels robotic)
  • Animate on every frame in scroll handlers