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-transitions.md
1# SwiftUI Transitions23Transitions for view insertion/removal, custom transitions, and the Animatable protocol.45## Table of Contents6- [Property Animations vs Transitions](#property-animations-vs-transitions)7- [Basic Transitions](#basic-transitions)8- [Asymmetric Transitions](#asymmetric-transitions)9- [Custom Transitions](#custom-transitions)10- [Identity and Transitions](#identity-and-transitions)11- [The Animatable Protocol](#the-animatable-protocol)1213---1415## Property Animations vs Transitions1617**Property animations**: Interpolate values on views that exist before AND after state change.1819**Transitions**: Animate views being inserted or removed from the render tree.2021```swift22// Property animation - same view, different properties23Rectangle()24.frame(width: isExpanded ? 200 : 100, height: 50)25.animation(.spring, value: isExpanded)2627// Transition - view inserted/removed28if showDetail {29DetailView()30.transition(.scale)31}32```3334---3536## Basic Transitions3738### Critical: Transitions Require Animation Context3940```swift41// GOOD - animation outside conditional42VStack {43Button("Toggle") { showDetail.toggle() }44if showDetail {45DetailView()46.transition(.slide)47}48}49.animation(.spring, value: showDetail)5051// GOOD - explicit animation52Button("Toggle") {53withAnimation(.spring) {54showDetail.toggle()55}56}57if showDetail {58DetailView()59.transition(.scale.combined(with: .opacity))60}6162// BAD - animation inside conditional (removed with view!)63if showDetail {64DetailView()65.transition(.slide)66.animation(.spring, value: showDetail) // Won't work on removal!67}6869// BAD - no animation context70Button("Toggle") {71showDetail.toggle() // No animation72}73if showDetail {74DetailView()75.transition(.slide) // Ignored - just appears/disappears76}77```7879### Built-in Transitions8081| Transition | Effect |82|------------|--------|83| `.opacity` | Fade in/out (default) |84| `.scale` | Scale up/down |85| `.slide` | Slide from leading edge |86| `.move(edge:)` | Move from specific edge |87| `.offset(x:y:)` | Move by offset amount |8889### Combining Transitions9091```swift92// Parallel - both simultaneously93.transition(.slide.combined(with: .opacity))9495// Chained96.transition(.scale.combined(with: .opacity).combined(with: .offset(y: 20)))97```9899---100101## Asymmetric Transitions102103Different animations for insertion vs removal.104105```swift106// GOOD - different animations for insert/remove107if showCard {108CardView()109.transition(110.asymmetric(111insertion: .scale.combined(with: .opacity),112removal: .move(edge: .bottom).combined(with: .opacity)113)114)115}116117// BAD - same transition when different behaviors needed118if showCard {119CardView()120.transition(.slide) // Same both ways - may feel awkward121}122```123124---125126## Custom Transitions127128### Pre-iOS 17129130```swift131struct BlurModifier: ViewModifier {132var radius: CGFloat133func body(content: Content) -> some View {134content.blur(radius: radius)135}136}137138extension AnyTransition {139static func blur(radius: CGFloat) -> AnyTransition {140.modifier(141active: BlurModifier(radius: radius),142identity: BlurModifier(radius: 0)143)144}145}146147// Usage148.transition(.blur(radius: 10))149```150151### iOS 17+ (Transition Protocol)152153```swift154struct BlurTransition: Transition {155var radius: CGFloat156157func body(content: Content, phase: TransitionPhase) -> some View {158content159.blur(radius: phase.isIdentity ? 0 : radius)160.opacity(phase.isIdentity ? 1 : 0)161}162}163164// Usage165.transition(BlurTransition(radius: 10))166```167168### Good vs Bad Custom Transitions169170```swift171// GOOD - reusable transition172if showContent {173ContentView()174.transition(BlurTransition(radius: 10))175}176177// BAD - inline logic (won't animate on removal!)178if showContent {179ContentView()180.blur(radius: showContent ? 0 : 10) // Not a transition181.opacity(showContent ? 1 : 0)182}183```184185---186187## Identity and Transitions188189View identity changes trigger transitions, not property animations.190191```swift192// Triggers transition - different branches have different identities193if isExpanded {194Rectangle().frame(width: 200, height: 50)195} else {196Rectangle().frame(width: 100, height: 50)197}198199// Triggers transition - .id() changes identity200Rectangle()201.id(flag) // Different identity when flag changes202.transition(.scale)203204// Property animation - same view, same identity205Rectangle()206.frame(width: isExpanded ? 200 : 100, height: 50)207.animation(.spring, value: isExpanded)208```209210---211212## The Animatable Protocol213214Enables custom property interpolation during animations.215216### Protocol Definition217218```swift219protocol Animatable {220associatedtype AnimatableData: VectorArithmetic221var animatableData: AnimatableData { get set }222}223```224225### Basic Implementation226227```swift228// GOOD - explicit animatableData229struct ShakeModifier: ViewModifier, Animatable {230var shakeCount: Double231232var animatableData: Double {233get { shakeCount }234set { shakeCount = newValue }235}236237func body(content: Content) -> some View {238content.offset(x: sin(shakeCount * .pi * 2) * 10)239}240}241242extension View {243func shake(count: Int) -> some View {244modifier(ShakeModifier(shakeCount: Double(count)))245}246}247248// Usage249Button("Shake") { shakeCount += 3 }250.shake(count: shakeCount)251.animation(.default, value: shakeCount)252253// BAD - missing animatableData (silent failure!)254struct BadShakeModifier: ViewModifier {255var shakeCount: Double256// Missing animatableData! Uses EmptyAnimatableData257258func body(content: Content) -> some View {259content.offset(x: sin(shakeCount * .pi * 2) * 10)260}261}262// Animation jumps to final value instead of interpolating263```264265### Multiple Properties with AnimatablePair266267```swift268// GOOD - AnimatablePair for two properties269struct ComplexModifier: ViewModifier, Animatable {270var scale: CGFloat271var rotation: Double272273var animatableData: AnimatablePair<CGFloat, Double> {274get { AnimatablePair(scale, rotation) }275set {276scale = newValue.first277rotation = newValue.second278}279}280281func body(content: Content) -> some View {282content283.scaleEffect(scale)284.rotationEffect(.degrees(rotation))285}286}287288// GOOD - nested AnimatablePair for 3+ properties289struct ThreePropertyModifier: ViewModifier, Animatable {290var x: CGFloat291var y: CGFloat292var rotation: Double293294var animatableData: AnimatablePair<AnimatablePair<CGFloat, CGFloat>, Double> {295get { AnimatablePair(AnimatablePair(x, y), rotation) }296set {297x = newValue.first.first298y = newValue.first.second299rotation = newValue.second300}301}302303func body(content: Content) -> some View {304content305.offset(x: x, y: y)306.rotationEffect(.degrees(rotation))307}308}309```310311---312313## Quick Reference314315### Do316- Place transitions outside conditional structures317- Use `withAnimation` or `.animation` outside the `if`318- Implement `animatableData` explicitly for custom Animatable319- Use `AnimatablePair` for multiple animated properties320- Use asymmetric transitions when insert/remove need different effects321322### Don't323- Put animation modifiers inside conditionals for transitions324- Forget `animatableData` implementation (silent failure)325- Use inline blur/opacity instead of proper transitions326- Expect property animation when view identity changes327