Use when implementing SwiftUI animations, micro-interactions, transitions, haptics, or gesture feedback in any iOS view. Applies to celebration effects, list stagger, number counters, loading states, navigation transitions, and particle effects.
A reference guide for building polished, production-grade animations in the iFlip iOS app. Covers primitive selection, critical gotchas, open-source patterns to steal, and app-specific conventions.
Core rule: Every animation must check UIAccessibility.isReduceMotionEnabled and fall back to .opacity only. No exceptions.
| You want to... | Use |
|---|---|
| Simple on/off toggle (show/hide, selection) | withAnimation(.snappy) or .animation(.spring, value:) |
| Multi-property sequence (scale + rotate + offset) | keyframeAnimator |
| Phase-based states (idle → burst → collapse) | phaseAnimator with an enum |
| Continuous render loop (particles, fill, pulse) | TimelineView(.animation) + Canvas |
| Number/text value changes | .contentTransition(.numericText()) — cross-fades digits elegantly |
| Morph one symbol into another | .contentTransition(.symbolEffect(.replace)) |
| Gesture with velocity preservation | .interactiveSpring during drag, non-interactive spring on release |
| View enter/exit | .transition(.asymmetric(...)) |
| Custom parameterized transition (stripe blinds, etc.) | Extend AnyTransition + GeometryEffect with custom Animatable params |
| Hero image between screens | matchedGeometryEffect |
| iOS 18 interactive zoom (drag mid-transition) | .navigationTransition(.zoom(sourceID:in:)) |
| Symbol icon animations | .symbolEffect(.bounce), .symbolEffect(.variableColor) |
| Pulsing incoming call / hue shift | .symbolEffect(.variableColor) + phaseAnimator hue rotation |
| Path drawing / dashes | trim(from:to:) or animated dashPhase |
| 3D rotation on individual characters | rotation3DEffect per character with staggered delay |
| Physics bounce off screen edge | Spring + clamp: min(max(offset, 0), maxBound) with .bouncy() |
| AI / image gen loading state | phaseAnimator cycling through engaging visual states |
Spring presets (prefer over .default):
.bouncy() — playful, high-energy (achievements, celebrations).smooth() — premium, no bounce (financial data, detail transitions).snappy — responsive selection feedback (filter chips, toggles).interpolatingSpring(stiffness:damping:) — full manual controlTransitions silently fail if triggered by implicit animations. Always wrap state changes in withAnimation {} OR attach animation to transition directly:
// ✅ Safe
.transition(.scale.animation(.bouncy()))
// ❌ Breaks silently
someView.animation(.bouncy()) // implicit — transition may not fire
Toast alerts and overlay sheets placed directly over a NavigationStack may disappear instantly without animating. Always present overlays from the root ZStack, not from inside a navigation destination.
Zoom transitions are interactive. If a user starts a pop while a push is still animating, the system force-completes the push immediately. Do not gate any logic on "transition in progress." Clean up all transition state in onAppear/onDisappear.
Complex grids and metric cards break at accessibility font sizes. Apply .minimumScaleFactor(0.7) on tight numeric displays and cap dynamic type with .dynamicTypeSize(...<= .xxxLarge) on widget grids.
Use a delegate protocol or @Published property on the service. The view wraps mutations in withAnimation. Never put animation logic in a Service layer.
Cloned at: C:\Users\wowth\Documents\projects\open-swiftui-animations
| File | Pattern | iFlip use |
|---|---|---|
JumpAndFallWithKeyframes.swift | keyframeAnimator: jump, rotate, fall | Achievement unlock, tier upgrade |
JumpAndFallWithPhase.swift | phaseAnimator enum: simpler jump/fall | Milestone pops (profit %, goal hit) |
XLike1.swift | Tap: fill + splash circles + counter | "Celebrate this flip" on item sold |
ReactionsView.swift | Staggered emoji buttons with phaseAnimator | Achievement reaction badges |
EmotionalReactions.swift | Multi-layer: stroke circle + splash + scale | Legendary tier unlock reveal |
| File | Use case |
|---|---|
Thinking.swift / CombinedSymbolEffects.swift | AI enrichment loading state |
IncreaseDecrease.swift | numericText() transition for +/- adjusters |
DuoLoading.swift | Animated character while AI scans |
AlertNotificationAnimation.swift | Bell + badge shake for alerts |
AnimationCompletionCriteria.swift | Chaining multi-step animation sequences |
MeasuringHeartRate.swift | Animated dashPhase stroke for "hot deal" pulse |
DesignSystem/Transitions.swift — .pushTransition / .popTransition defined but currently unused in NavigationStack destinations. Apply these.DesignSystem/DesignSystem.swift — Colors.forRarity(_:) maps AchievementRarity to token color. Always use this, never hardcode hex.Services/GamificationManager.swift — uses GamificationVisualDelegate protocol to keep SwiftUI out of Services.Services/HapticManager.swift — triggerStandardHaptics(for rarity:) exists. Use it.| Tier | Animation style | Haptic |
|---|---|---|
| Common | Toast spring bounce | Light |
| Rare | Toast + scale pop | Medium |
| Epic | Full overlay, particles | Heavy |
| Legendary | Full overlay, extended burst | Rigid |
| Unicorn / Unobtanium | Maximum everything | Rigid × 2 |
There are currently no centralized timing tokens. Until they exist, use these values consistently:
response: 0.3, dampingFraction: 0.7response: 0.4, dampingFraction: 0.6.bouncy(duration: 0.5, extraBounce: 0.1)response: 0.6, dampingFraction: 0.85Pair every meaningful interaction with haptics. Use .sensoryFeedback() modifier (iOS 17+) over manual UIImpactFeedbackGenerator where possible:
.sensoryFeedback(.impact(flexibility: .soft), trigger: selectedFilter)
.sensoryFeedback(.success, trigger: itemSold)
.sensoryFeedback(.warning, trigger: deleteConfirmed)
Missing haptics in iFlip (add these): filter chip taps, swipe action completions, list item taps, achievement badge taps, status changes.
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
ItemRow(item: item)
.transition(.scale(scale: 0.95).combined(with: .opacity))
.animation(
.spring(response: 0.5, dampingFraction: 0.8)
.delay(Double(index) * 0.04),
value: items.count
)
}
Cap delay at ~20 items (min(index, 20) * 0.04) to avoid long waits on large lists.
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
content
.animation(reduceMotion ? .none : .bouncy(), value: isVisible)
}
For Canvas-based particles: skip the TimelineView entirely and show a static icon + opacity fade.
Shape-clipping cross-fade: Use an animated shape to clip incoming/outgoing views for custom cross-effects instead of plain opacity. Apply to photo preview transitions and item detail reveals.
Custom parameterized transition (stripe blinds):
extension AnyTransition {
static func stripes(count: Int, horizontal: Bool) -> AnyTransition {
.modifier(active: StripesModifier(count: count, horizontal: horizontal, progress: 0),
identity: StripesModifier(count: count, horizontal: horizontal, progress: 1))
}
}
// StripesModifier conforms to Animatable + ViewModifier, clips view into N animated stripes
iOS 18 interactive zoom (mid-transition drag):
// Source view
.matchedTransitionSource(id: item.id, in: namespace)
// Destination
.navigationTransition(.zoom(sourceID: item.id, in: namespace))
// User can grab and drag mid-transition — do NOT gate logic on transition state
GeometryEffect custom modal: Extend AnyTransition + GeometryEffect for fully custom modal present/dismiss (e.g., item detail expanding from list row).
Velocity-preserving fling:
.gesture(DragGesture()
.onChanged { value in
offset = value.translation // .interactiveSpring updates target continuously
}
.onEnded { _ in
withAnimation(.spring()) { offset = finalPosition } // preserves velocity
}
)
Animated path drawing: trim(from: 0, to: progress) on a Path, animate progress 0→1. Use for goal completion checkmark stroke animation.
Per-character 3D flip:
ForEach(Array(text.enumerated()), id: \.offset) { index, char in
Text(String(char))
.rotation3DEffect(isRevealed ? .zero : .degrees(90), axis: (0, 1, 0))
.animation(.bouncy().delay(Double(index) * 0.05), value: isRevealed)
}
AI loading state with phaseAnimator: Instead of a spinner, cycle through engaging phases (scanning → thinking → analyzing → almost done) with visual transitions between each. Keeps user engaged during enrichment pipeline.
Anchored rotation: Specify .rotationEffect(angle, anchor: .bottomLeading) to rotate around a custom point rather than center. Good for "flip card" or "door open" effects on item cards.
| Mistake | Fix |
|---|---|
withAnimation inside a Service | Use delegate pattern — view owns animation calls |
| Hardcoded hex in animation color | Use DesignSystem.Colors.forRarity(_:) |
Overlay inside NavigationStack | Move to root ZStack |
| No reduce motion fallback | Always check accessibilityReduceMotion |
.animation(.default) | Use named spring: .bouncy(), .smooth(), .snappy |
| Large stagger delay on long lists | Cap at min(index, 20) * delay |
import SwiftUI in a Service | Protocol/delegate pattern only |
| Transition not animating | Wrap trigger in withAnimation {} — implicit animations break transitions |