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-advanced.md
1# SwiftUI Advanced Animations23Transactions, phase animations (iOS 17+), keyframe animations (iOS 17+), completion handlers (iOS 17+), and `@Animatable` macro (iOS 26+).45## Table of Contents6- [Transactions](#transactions)7- [Phase Animations (iOS 17+)](#phase-animations-ios-17)8- [Keyframe Animations (iOS 17+)](#keyframe-animations-ios-17)9- [Animation Completion Handlers (iOS 17+)](#animation-completion-handlers-ios-17)10- [@Animatable Macro (iOS 26+)](#animatable-macro-ios-26)1112---1314## Transactions1516The underlying mechanism for all animations in SwiftUI.1718### Basic Usage1920```swift21// withAnimation is shorthand for withTransaction22withAnimation(.default) { flag.toggle() }2324// Equivalent explicit transaction25var transaction = Transaction(animation: .default)26withTransaction(transaction) { flag.toggle() }27```2829### The .transaction Modifier3031```swift32Rectangle()33.frame(width: flag ? 100 : 50, height: 50)34.transaction { t in35t.animation = .default36}37```3839**Note:** This behaves like the deprecated `.animation(_:)` without value parameter - it animates on every state change.4041### Animation Precedence4243**Implicit animations override explicit animations** (later in view tree wins).4445```swift46Button("Tap") {47withAnimation(.linear) { flag.toggle() }48}49.animation(.bouncy, value: flag) // .bouncy wins!50```5152### Disabling Animations5354```swift55// Prevent implicit animations from overriding56.transaction { t in57t.disablesAnimations = true58}5960// Remove animation entirely61.transaction { $0.animation = nil }62```6364### Custom Transaction Keys (iOS 17+)6566Pass metadata through transactions.6768```swift69struct ChangeSourceKey: TransactionKey {70static let defaultValue: String = "unknown"71}7273extension Transaction {74var changeSource: String {75get { self[ChangeSourceKey.self] }76set { self[ChangeSourceKey.self] = newValue }77}78}7980// Set source81var transaction = Transaction(animation: .default)82transaction.changeSource = "server"83withTransaction(transaction) { flag.toggle() }8485// Read in view tree86.transaction { t in87if t.changeSource == "server" {88t.animation = .smooth89} else {90t.animation = .bouncy91}92}93```9495---9697## Phase Animations (iOS 17+)9899Cycle through discrete phases automatically. Each phase change is a separate animation.100101### Basic Usage102103```swift104// GOOD - triggered phase animation105Button("Shake") { trigger += 1 }106.phaseAnimator(107[0.0, -10.0, 10.0, -5.0, 5.0, 0.0],108trigger: trigger109) { content, offset in110content.offset(x: offset)111}112113// Infinite loop (no trigger)114Circle()115.phaseAnimator([1.0, 1.2, 1.0]) { content, scale in116content.scaleEffect(scale)117}118```119120### Enum Phases (Recommended for Clarity)121122```swift123// GOOD - enum phases are self-documenting124enum BouncePhase: CaseIterable {125case initial, up, down, settle126127var scale: CGFloat {128switch self {129case .initial: 1.0130case .up: 1.2131case .down: 0.9132case .settle: 1.0133}134}135}136137Circle()138.phaseAnimator(BouncePhase.allCases, trigger: trigger) { content, phase in139content.scaleEffect(phase.scale)140}141```142143### Custom Timing Per Phase144145```swift146.phaseAnimator([0, -20, 20], trigger: trigger) { content, offset in147content.offset(x: offset)148} animation: { phase in149switch phase {150case -20: .bouncy151case 20: .linear152default: .smooth153}154}155```156157### Good vs Bad158159```swift160// GOOD - use phaseAnimator for multi-step sequences161.phaseAnimator([0, -10, 10, 0], trigger: trigger) { content, offset in162content.offset(x: offset)163}164165// BAD - manual DispatchQueue sequencing166Button("Animate") {167withAnimation(.easeOut(duration: 0.1)) { offset = -10 }168DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {169withAnimation { offset = 10 }170}171DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {172withAnimation { offset = 0 }173}174}175```176177---178179## Keyframe Animations (iOS 17+)180181Precise timing control with exact values at specific times.182183### Basic Usage184185```swift186Button("Bounce") { trigger += 1 }187.keyframeAnimator(188initialValue: AnimationValues(),189trigger: trigger190) { content, value in191content192.scaleEffect(value.scale)193.offset(y: value.verticalOffset)194} keyframes: { _ in195KeyframeTrack(\.scale) {196SpringKeyframe(1.2, duration: 0.15)197SpringKeyframe(0.9, duration: 0.1)198SpringKeyframe(1.0, duration: 0.15)199}200KeyframeTrack(\.verticalOffset) {201LinearKeyframe(-20, duration: 0.15)202LinearKeyframe(0, duration: 0.25)203}204}205206struct AnimationValues {207var scale: CGFloat = 1.0208var verticalOffset: CGFloat = 0209}210```211212### Keyframe Types213214| Type | Behavior |215|------|----------|216| `CubicKeyframe` | Smooth interpolation |217| `LinearKeyframe` | Straight-line interpolation |218| `SpringKeyframe` | Spring physics |219| `MoveKeyframe` | Instant jump (no interpolation) |220221### Multiple Synchronized Tracks222223Tracks run **in parallel**, each animating one property.224225```swift226// GOOD - bell shake with synchronized rotation and scale227struct BellAnimation {228var rotation: Double = 0229var scale: CGFloat = 1.0230}231232Image(systemName: "bell.fill")233.keyframeAnimator(234initialValue: BellAnimation(),235trigger: trigger236) { content, value in237content238.rotationEffect(.degrees(value.rotation))239.scaleEffect(value.scale)240} keyframes: { _ in241KeyframeTrack(\.rotation) {242CubicKeyframe(15, duration: 0.1)243CubicKeyframe(-15, duration: 0.1)244CubicKeyframe(10, duration: 0.1)245CubicKeyframe(-10, duration: 0.1)246CubicKeyframe(0, duration: 0.1)247}248KeyframeTrack(\.scale) {249CubicKeyframe(1.1, duration: 0.25)250CubicKeyframe(1.0, duration: 0.25)251}252}253254// BAD - manual timer-based animation255Image(systemName: "bell.fill")256.onTapGesture {257withAnimation(.easeOut(duration: 0.1)) { rotation = 15 }258DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {259withAnimation { rotation = -15 }260}261// ... more manual timing - error prone262}263```264265### KeyframeTimeline (iOS 17+)266267Query animation values directly for testing or non-SwiftUI use.268269```swift270let timeline = KeyframeTimeline(initialValue: AnimationValues()) {271KeyframeTrack(\.scale) {272CubicKeyframe(1.2, duration: 0.25)273CubicKeyframe(1.0, duration: 0.25)274}275}276277let midpoint = timeline.value(time: 0.25)278print(midpoint.scale) // Value at 0.25 seconds279```280281---282283## Animation Completion Handlers (iOS 17+)284285Execute code when animations finish.286287### With withAnimation288289```swift290// GOOD - completion with withAnimation291Button("Animate") {292withAnimation(.spring) {293isExpanded.toggle()294} completion: {295showNextStep = true296}297}298```299300### With Transaction (For Reexecution)301302```swift303// GOOD - completion fires on every trigger change304Circle()305.scaleEffect(bounceCount % 2 == 0 ? 1.0 : 1.2)306.transaction(value: bounceCount) { transaction in307transaction.animation = .spring308transaction.addAnimationCompletion {309message = "Bounce \(bounceCount) complete"310}311}312313// BAD - completion only fires ONCE (no value parameter)314Circle()315.scaleEffect(bounceCount % 2 == 0 ? 1.0 : 1.2)316.animation(.spring, value: bounceCount)317.transaction { transaction in // No value!318transaction.addAnimationCompletion {319completionCount += 1 // Only fires once, ever320}321}322```323324---325326## @Animatable Macro (iOS 26+)327328The `@Animatable` macro auto-synthesizes `animatableData` from all animatable stored properties, eliminating verbose manual conformance. Use `@AnimatableIgnored` to exclude properties that should not animate.329330### Before (Manual)331332```swift333struct Wedge: Shape {334var startAngle: Angle335var endAngle: Angle336var drawClockwise: Bool337338var animatableData: AnimatablePair<Double, Double> {339get { AnimatablePair(startAngle.radians, endAngle.radians) }340set {341startAngle = .radians(newValue.first)342endAngle = .radians(newValue.second)343}344}345346func path(in rect: CGRect) -> Path { /* ... */ }347}348```349350### After (@Animatable)351352```swift353@Animatable354struct Wedge: Shape {355var startAngle: Angle356var endAngle: Angle357@AnimatableIgnored var drawClockwise: Bool358359func path(in rect: CGRect) -> Path { /* ... */ }360}361```362363### When to Use364- **Prefer `@Animatable`** for any custom `Shape`, `AnimatableModifier`, or type conforming to `Animatable` with multiple properties365- **Use `@AnimatableIgnored`** for properties that control behavior but should not interpolate (e.g., directions, flags, identifiers)366- The macro works with any type conforming to `Animatable`, not just `Shape`367368> Source: "What's new in SwiftUI" (WWDC25, session 256)369370---371372## Quick Reference373374### Transactions (All iOS versions)375- `withTransaction` is the explicit form of `withAnimation`376- Implicit animations override explicit (later in view tree wins)377- Use `disablesAnimations` to prevent override378- Use `.transaction { $0.animation = nil }` to remove animation379380### Custom Transaction Keys (iOS 17+)381- Pass metadata through animation system via `TransactionKey`382383### Phase Animations (iOS 17+)384- Use for multi-step sequences returning to start385- Prefer enum phases for clarity386- Each phase change is a separate animation387- Use `trigger` parameter for one-shot animations388389### Keyframe Animations (iOS 17+)390- Use for precise timing control391- Tracks run in parallel392- Use `KeyframeTimeline` for testing/advanced use393- Prefer over manual DispatchQueue timing394395### Completion Handlers (iOS 17+)396- Use `withAnimation(.animation) { } completion: { }` for one-shot completion handlers397- Use `.transaction(value:)` for handlers that should refire on every value change398- Without `value:` parameter, completion only fires once399400### @Animatable Macro (iOS 26+)401- Use `@Animatable` to auto-synthesize `animatableData` from stored properties402- Use `@AnimatableIgnored` to exclude non-animatable properties403- Replaces verbose manual `animatableData` getters/setters404