SwiftUI Animation Basics
SwiftUI Animation Basics
Section titled “SwiftUI Animation Basics”Core animation concepts, implicit vs explicit animations, timing curves, and performance patterns.
Table of Contents
Section titled “Table of Contents”- Core Concepts
- Implicit Animations
- Explicit Animations
- Animation Placement
- Selective Animation
- Timing Curves
- Animation Performance
- Disabling Animations
- Debugging
Core Concepts
Section titled “Core Concepts”State changes trigger view updates. SwiftUI provides mechanisms to animate these changes.
Animation Process:
- State change triggers view tree re-evaluation
- SwiftUI compares new tree to current render tree
- 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
Section titled “Implicit Animations”Use .animation(_:value:) to animate when a specific value changes.
// GOOD - uses value parameterRectangle() .frame(width: isExpanded ? 200 : 100, height: 50) .animation(.spring, value: isExpanded) .onTapGesture { isExpanded.toggle() }
// BAD - deprecated, animates all changes unexpectedlyRectangle() .frame(width: isExpanded ? 200 : 100, height: 50) .animation(.spring) // Deprecated!Explicit Animations
Section titled “Explicit Animations”Use withAnimation for event-driven state changes.
// GOOD - explicit animationButton("Toggle") { withAnimation(.spring) { isExpanded.toggle() }}
// BAD - no animation contextButton("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
Section titled “Animation Placement”Place animation modifiers after the properties they should animate.
// GOOD - animation after propertiesRectangle() .frame(width: isExpanded ? 200 : 100, height: 50) .foregroundStyle(isExpanded ? .blue : .red) .animation(.default, value: isExpanded) // Animates both
// BAD - animation before propertiesRectangle() .animation(.default, value: isExpanded) // Too early! .frame(width: isExpanded ? 200 : 100, height: 50)Selective Animation
Section titled “Selective Animation”Animate only specific properties using multiple animation modifiers or scoped animations.
// GOOD - selective animationRectangle() .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 animationRectangle() .foregroundStyle(isExpanded ? .blue : .red) // Not animated .animation(.spring) { $0.frame(width: isExpanded ? 200 : 100, height: 50) // Animated }Timing Curves
Section titled “Timing Curves”Built-in Curves
Section titled “Built-in Curves”| Curve | Use Case |
|---|---|
.spring |
Interactive elements, most UI |
.easeInOut |
Appearance changes |
.bouncy |
Playful feedback (iOS 17+) |
.linear |
Progress indicators only |
Modifiers
Section titled “Modifiers”.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
Section titled “Good vs Bad Timing”// GOOD - appropriate timing for interaction typeButton("Tap") { withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { isActive.toggle() }}.scaleEffect(isActive ? 0.95 : 1.0)
// BAD - too slow for button feedbackButton("Tap") { withAnimation(.easeInOut(duration: 1.0)) { // Way too slow! isActive.toggle() }}
// BAD - linear feels roboticRectangle() .animation(.linear(duration: 0.5), value: isActive) // MechanicalAnimation Performance
Section titled “Animation Performance”Prefer Transforms Over Layout
Section titled “Prefer Transforms Over Layout”// GOOD - GPU accelerated transformsRectangle() .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 expensiveRectangle() .frame(width: isActive ? 150 : 100, height: isActive ? 150 : 100) // Expensive .padding(isActive ? 50 : 0) // ExpensiveNarrow Animation Scope
Section titled “Narrow Animation Scope”// GOOD - animation scoped to specific subviewVStack { HeaderView() // Not affected ExpandableContent(isExpanded: isExpanded) .animation(.spring, value: isExpanded) // Only this FooterView() // Not affected}
// BAD - animation at rootVStack { HeaderView() ExpandableContent(isExpanded: isExpanded) FooterView()}.animation(.spring, value: isExpanded) // Animates everythingAvoid Animation in Hot Paths
Section titled “Avoid Animation in Hot Paths”// 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
Section titled “Disabling Animations”// GOOD - disable with transactionText("Count: \(count)") .transaction { $0.animation = nil }
// GOOD - disable from parent contextDataView() .transaction { $0.disablesAnimations = true }
// BAD - hacky zero durationText("Count: \(count)") .animation(.linear(duration: 0), value: count) // HackyDebugging
Section titled “Debugging”// 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 valuesstruct 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
Section titled “Quick Reference”- Use
.animation(_:value:)with value parameter - Use
withAnimationfor event-driven animations - Prefer transforms over layout changes
- Scope animations narrowly
- Choose appropriate timing curves
- 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