SwiftUI & AppKit Animation Best Practices
Comprehensive animation guide for Apple platform interfaces, adapted from Emil Kowalski's web animation principles and Framer Motion best practices. Contains 80 rules across 10 categories targeting iOS 17+, prioritized by impact.
When to Apply
Reference these guidelines when:
-
Adding animations to SwiftUI views
-
Choosing easing curves, springs, or timing values
-
Implementing gesture-based interactions (drag, tap, long press)
-
Building transitions and navigation animations
-
Using matchedGeometryEffect for shared element transitions
-
Creating scroll-linked or parallax effects
-
Using iOS 17+ PhaseAnimator or KeyframeAnimator
-
Optimizing animation performance
-
Ensuring animation accessibility with accessibilityReduceMotion
-
Writing AppKit/macOS-specific animations
Rule Categories by Priority
Priority Category Impact Prefix
1 Timing Curves & Easing CRITICAL ease-
2 Duration & Timing CRITICAL timing-
3 Animation Properties HIGH props-
4 Transforms & Effects HIGH transform-
5 Gesture & Interaction HIGH gesture-
6 Transitions & Navigation MEDIUM-HIGH transition-
7 Scroll & Parallax MEDIUM scroll-
8 Strategic Animation MEDIUM strategy-
9 Accessibility & Polish MEDIUM polish-
10 AppKit Specific LOW-MEDIUM appkit-
Quick Reference
- Timing Curves & Easing (CRITICAL)
-
ease-prefer-easeout
-
Use easeOut as your default easing
-
ease-avoid-linear
-
Avoid linear easing for UI animations
-
ease-spring-default
-
Prefer springs for natural motion
-
ease-spring-ios17
-
Use iOS 17 Spring struct with bounce/duration
-
ease-custom-timing-curve
-
Create custom curves with timingCurve
-
ease-ease-in-for-exits
-
Use easeIn for exit animations
-
ease-ease-inout-for-emphasis
-
Use easeInOut for emphasis motion
-
ease-spring-response
-
Configure spring response for duration feel
-
ease-spring-damping
-
Configure spring damping for bounce
-
ease-match-context
-
Match easing to animation context
- Duration & Timing (CRITICAL)
-
timing-200ms-default
-
Use 200ms as default UI animation duration
-
timing-100ms-micro
-
Use 100ms for micro-interactions
-
timing-300ms-emphasis
-
Keep emphasis animations under 300ms
-
timing-stagger-children
-
Stagger child animations for orchestration
-
timing-delay-strategic
-
Use delay strategically for sequencing
- Animation Properties (HIGH)
-
props-opacity-scale
-
Prefer opacity and scale for performance
-
props-avoid-size-animate
-
Avoid animating frame size directly
-
props-transform-origin
-
Set anchor points for transforms
-
props-drawing-group
-
Use drawingGroup for complex hierarchies
-
props-animation-disable
-
Use animation(nil) to prevent animations
-
props-explicit-animation
-
Use withAnimation for explicit control
-
props-implicit-animation
-
Use .animation modifier for implicit animations
- Transforms & Effects (HIGH)
-
transform-scale-subtle
-
Use subtle scale values (0.95-0.98)
-
transform-rotation-purposeful
-
Apply rotation with purpose
-
transform-translate-direction
-
Translate in meaningful directions
-
transform-3d-perspective
-
Use rotation3DEffect for depth
-
transform-order-matters
-
Modifier order affects output
-
transform-anchor-point
-
Set anchor for rotation/scale origin
-
transform-combine-effects
-
Combine transforms purposefully
- Gesture & Interaction (HIGH)
-
gesture-tap-feedback
-
Provide immediate tap feedback
-
gesture-long-press
-
Animate long press states
-
gesture-drag-basic
-
Implement smooth drag interactions
-
gesture-drag-constraints
-
Constrain drag within bounds
-
gesture-drag-velocity
-
Use velocity for momentum effects
-
gesture-gesture-state
-
Use @GestureState for transient values
-
gesture-updating-modifier
-
Use .updating for live feedback
-
gesture-simultaneous-vs-exclusive
-
Choose gesture composition
-
gesture-magnify-gesture
-
Implement pinch-to-zoom
-
gesture-rotation-gesture
-
Implement rotation gestures
-
gesture-hover-macos
-
Handle hover on macOS
-
gesture-sensory-feedback
-
Pair gestures with haptics
-
gesture-spring-on-release
-
Spring back on gesture end
-
gesture-cancellation
-
Handle gesture cancellation gracefully
- Transitions & Navigation (MEDIUM-HIGH)
-
transition-builtin-types
-
Use built-in transition types
-
transition-asymmetric
-
Use asymmetric for different enter/exit
-
transition-combined
-
Combine transitions for richer effects
-
transition-custom-modifier
-
Create custom transition modifiers
-
transition-matched-geometry
-
Use matchedGeometryEffect properly
-
transition-namespace-scope
-
Manage @Namespace lifecycle
-
transition-navigation-transitions
-
Customize navigation transitions
-
transition-sheet-presentations
-
Animate sheet presentations
-
transition-id-for-replacement
-
Use .id() for view replacement
-
transition-content-transition
-
Use contentTransition for text
- Scroll & Parallax (MEDIUM)
-
scroll-geometry-reader
-
Use GeometryReader for scroll position
-
scroll-preference-key
-
Use PreferenceKey for scroll data
-
scroll-parallax-effect
-
Create parallax scroll effects
-
scroll-sticky-header
-
Implement animated sticky headers
-
scroll-scroll-transition
-
Use iOS 17 scrollTransition modifier
-
scroll-visual-effect
-
Use iOS 17 visualEffect modifier
- Strategic Animation (MEDIUM)
-
strategy-purposeful-motion
-
Every animation needs purpose
-
strategy-hierarchy-emphasis
-
Use motion to show hierarchy
-
strategy-state-communication
-
Animate state changes clearly
-
strategy-spatial-continuity
-
Maintain spatial relationships
-
strategy-brand-expression
-
Express brand through motion
- Accessibility & Polish (MEDIUM)
-
polish-reduce-motion
-
Respect accessibilityReduceMotion
-
polish-phase-animator
-
Use PhaseAnimator for sequences
-
polish-keyframe-animator
-
Use KeyframeAnimator for complex paths
-
polish-animation-completions
-
Handle animation completion
-
polish-interruptible-animations
-
Make animations interruptible
-
polish-animation-debugging
-
Debug animations effectively
-
polish-performance-profiling
-
Profile animation performance
-
polish-symbol-effects
-
Use SF Symbol effects
-
polish-text-animations
-
Animate text with contentTransition
-
polish-haptic-pairing
-
Pair animations with haptic feedback
- AppKit Specific (LOW-MEDIUM)
-
appkit-nsanimation-context
-
Use NSAnimationContext for grouping
-
appkit-core-animation
-
Use Core Animation layers
-
appkit-layer-backed-views
-
Enable layer backing for performance
-
appkit-implicit-animations
-
Leverage implicit layer animations
-
appkit-spring-animation
-
Create spring animations in AppKit
-
appkit-animator-proxy
-
Use animator proxy for view animations
Key Values Reference
Value Usage
.spring(duration: 0.3, bounce: 0.2)
Standard iOS 17 spring animation
.spring(response: 0.3, dampingFraction: 0.7)
Classic spring configuration
.easeOut(duration: 0.2)
Standard UI transition
scaleEffect(0.97)
Button press feedback
scaleEffect(0.95)
Minimum enter scale (never scale to 0)
0.2 seconds Default micro-interaction duration
0.3 seconds Maximum duration for UI animations
0.5 seconds Sheet/drawer animation duration
Concept Mapping (Web to SwiftUI)
Web Concept SwiftUI Equivalent
ease-out
.easeOut or Animation.easeOut(duration:)
cubic-bezier(a,b,c,d)
.timingCurve(a, b, c, d, duration:)
Framer Motion spring .spring(duration:bounce:) (iOS 17+)
transform: scale(0.97)
.scaleEffect(0.97)
transform-origin
.scaleEffect(anchor: .topLeading)
useMotionValue
@State / @GestureState
AnimatePresence
.transition()
- .id()
layoutId
.matchedGeometryEffect(id:in:)
useScroll
GeometryReader
- PreferenceKey
whileHover/whileTap
.onHover {} / gesture modifiers
dragConstraints
DragGesture with onChange bounds
prefers-reduced-motion
@Environment(.accessibilityReduceMotion)
Reference Files
File Description
references/_sections.md Category definitions and ordering
assets/templates/_template.md Template for new rules
metadata.json Version and reference information