Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/animation-basics.md
1# SwiftUI Animation Basics23Core animation concepts, implicit vs explicit animations, timing curves, and performance patterns.45## Table of Contents6- [Core Concepts](#core-concepts)7- [Implicit Animations](#implicit-animations)8- [Explicit Animations](#explicit-animations)9- [Animation Placement](#animation-placement)10- [Selective Animation](#selective-animation)11- [Timing Curves](#timing-curves)12- [Animation Performance](#animation-performance)13- [Disabling Animations](#disabling-animations)14- [Debugging](#debugging)1516---1718## Core Concepts1920State changes trigger view updates. SwiftUI provides mechanisms to animate these changes.2122**Animation Process:**231. State change triggers view tree re-evaluation242. SwiftUI compares new tree to current render tree253. Animatable properties are identified and interpolated (~60 fps)2627**Key Characteristics:**28- Animations are additive and cancelable29- Always start from current render tree state30- Blend smoothly when interrupted3132---3334## Implicit Animations3536Use `.animation(_:value:)` to animate when a specific value changes.3738```swift39// GOOD - uses value parameter40Rectangle()41.frame(width: isExpanded ? 200 : 100, height: 50)42.animation(.spring, value: isExpanded)43.onTapGesture { isExpanded.toggle() }4445// BAD - deprecated, animates all changes unexpectedly46Rectangle()47.frame(width: isExpanded ? 200 : 100, height: 50)48.animation(.spring) // Deprecated!49```5051---5253## Explicit Animations5455Use `withAnimation` for event-driven state changes.5657```swift58// GOOD - explicit animation59Button("Toggle") {60withAnimation(.spring) {61isExpanded.toggle()62}63}6465// BAD - no animation context66Button("Toggle") {67isExpanded.toggle() // Abrupt change68}69```7071**When to use which:**72- **Implicit**: Animations tied to specific value changes, precise view tree scope73- **Explicit**: Event-driven animations (button taps, gestures)7475---7677## Animation Placement7879Place animation modifiers after the properties they should animate.8081```swift82// GOOD - animation after properties83Rectangle()84.frame(width: isExpanded ? 200 : 100, height: 50)85.foregroundStyle(isExpanded ? .blue : .red)86.animation(.default, value: isExpanded) // Animates both8788// BAD - animation before properties89Rectangle()90.animation(.default, value: isExpanded) // Too early!91.frame(width: isExpanded ? 200 : 100, height: 50)92```9394---9596## Selective Animation9798Animate only specific properties using multiple animation modifiers or scoped animations.99100```swift101// GOOD - selective animation102Rectangle()103.frame(width: isExpanded ? 200 : 100, height: 50)104.animation(.spring, value: isExpanded) // Animate size105.foregroundStyle(isExpanded ? .blue : .red)106.animation(nil, value: isExpanded) // Don't animate color107108// iOS 17+ scoped animation109Rectangle()110.foregroundStyle(isExpanded ? .blue : .red) // Not animated111.animation(.spring) {112$0.frame(width: isExpanded ? 200 : 100, height: 50) // Animated113}114```115116---117118## Timing Curves119120### Built-in Curves121122| Curve | Use Case |123|-------|----------|124| `.spring` | Interactive elements, most UI |125| `.easeInOut` | Appearance changes |126| `.bouncy` | Playful feedback (iOS 17+) |127| `.linear` | Progress indicators only |128129### Modifiers130131```swift132.animation(.default.speed(2.0), value: flag) // 2x faster133.animation(.default.delay(0.5), value: flag) // Delayed start134.animation(.default.repeatCount(3, autoreverses: true), value: flag)135```136137### Good vs Bad Timing138139```swift140// GOOD - appropriate timing for interaction type141Button("Tap") {142withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {143isActive.toggle()144}145}146.scaleEffect(isActive ? 0.95 : 1.0)147148// BAD - too slow for button feedback149Button("Tap") {150withAnimation(.easeInOut(duration: 1.0)) { // Way too slow!151isActive.toggle()152}153}154155// BAD - linear feels robotic156Rectangle()157.animation(.linear(duration: 0.5), value: isActive) // Mechanical158```159160---161162## Animation Performance163164### Prefer Transforms Over Layout165166```swift167// GOOD - GPU accelerated transforms168Rectangle()169.frame(width: 100, height: 100)170.scaleEffect(isActive ? 1.5 : 1.0) // Fast171.offset(x: isActive ? 50 : 0) // Fast172.rotationEffect(.degrees(isActive ? 45 : 0)) // Fast173.animation(.spring, value: isActive)174175// BAD - layout changes are expensive176Rectangle()177.frame(width: isActive ? 150 : 100, height: isActive ? 150 : 100) // Expensive178.padding(isActive ? 50 : 0) // Expensive179```180181### Narrow Animation Scope182183```swift184// GOOD - animation scoped to specific subview185VStack {186HeaderView() // Not affected187ExpandableContent(isExpanded: isExpanded)188.animation(.spring, value: isExpanded) // Only this189FooterView() // Not affected190}191192// BAD - animation at root193VStack {194HeaderView()195ExpandableContent(isExpanded: isExpanded)196FooterView()197}198.animation(.spring, value: isExpanded) // Animates everything199```200201### Avoid Animation in Hot Paths202203```swift204// GOOD - gate by threshold205.onPreferenceChange(ScrollOffsetKey.self) { offset in206let shouldShow = offset.y < -50207if shouldShow != showTitle { // Only when crossing threshold208withAnimation(.easeOut(duration: 0.2)) {209showTitle = shouldShow210}211}212}213214// BAD - animating every scroll change215.onPreferenceChange(ScrollOffsetKey.self) { offset in216withAnimation { // Fires constantly!217self.offset = offset.y218}219}220```221222---223224## Disabling Animations225226```swift227// GOOD - disable with transaction228Text("Count: \(count)")229.transaction { $0.animation = nil }230231// GOOD - disable from parent context232DataView()233.transaction { $0.disablesAnimations = true }234235// BAD - hacky zero duration236Text("Count: \(count)")237.animation(.linear(duration: 0), value: count) // Hacky238```239240---241242## Debugging243244```swift245// Slow down for inspection246#if DEBUG247.animation(.linear(duration: 3.0).speed(0.2), value: isExpanded)248#else249.animation(.spring, value: isExpanded)250#endif251252// Debug modifier to log values253struct AnimationDebugModifier: ViewModifier, Animatable {254var value: Double255var animatableData: Double {256get { value }257set {258value = newValue259print("Animation: \(newValue)")260}261}262func body(content: Content) -> some View {263content.opacity(value)264}265}266```267268---269270## Quick Reference271272### Do273- Use `.animation(_:value:)` with value parameter274- Use `withAnimation` for event-driven animations275- Prefer transforms over layout changes276- Scope animations narrowly277- Choose appropriate timing curves278279### Don't280- Use deprecated `.animation(_:)` without value281- Animate layout properties in hot paths282- Apply broad animations at root level283- Use linear timing for UI (feels robotic)284- Animate on every frame in scroll handlers285