SwiftUI animations feel simple on the surface:
withAnimation {
state.toggle()
}
But under the hood, a lot more is happening.
SwiftUI doesn’t “animate views” — it propagates animation intent through a transaction system that controls:
- which changes animate
- which changes don’t
- how multiple animations merge
- how interruptions work
- how layout, rendering, and state updates stay in sync
If you’ve ever wondered:
- why some animations don’t run
- why others unexpectedly animate
- why animations cancel or jump
- why nested animations behave strangely
…the answer is almost always transactions.
This post explains how SwiftUI animation transactions really work, and how to control them intentionally.
🧠 What Is a Transaction?
A Transaction is a context object SwiftUI uses to describe how state changes should be applied.
It contains things like:
- animation (or no animation)
- animation timing
- whether animations are disabled
- how updates propagate
Every SwiftUI update runs inside a transaction — even when you don’t specify one.
🔄 The Core Update Flow
When state changes:
State Mutation
↓
Transaction Created
↓
Transaction Propagates Down View Tree
↓
Views Read Transaction
↓
Layout + Render + Animate
Animations are not “attached to views” —
they are instructions carried by the transaction.
🎬 withAnimation Is Just a Transaction Wrapper
This:
withAnimation(.spring()) {
isExpanded.toggle()
}
Is conceptually equivalent to:
var transaction = Transaction(animation: .spring())
transaction.perform {
isExpanded.toggle()
}
SwiftUI:
- creates a transaction
- attaches the animation
- applies state changes
- propagates that animation to all affected views
🧩 Implicit vs Explicit Animations
Implicit animation
Text("Hello")
.opacity(isVisible ? 1 : 0)
.animation(.easeInOut, value: isVisible)
This attaches an animation to that view subtree only.
Explicit animation
withAnimation {
isVisible.toggle()
}
This applies the animation globally to all state changes inside the block.
Rule of thumb:
-
withAnimation→ broad, global intent -
.animation(_, value:)→ scoped, local intent
🧱 How Transactions Propagate
Transactions flow top-down.
If a parent view has an animation transaction:
- all children inherit it
- unless they override or disable it
Example:
withAnimation(.easeInOut) {
state.toggle()
}
Every affected view animates — opacity, position, size, layout — unless explicitly stopped.
⛔ Disabling Animations with Transactions
Sometimes you want state updates without animation, even inside an animated context.
Transaction(animation: nil).perform {
state = newValue
}
Or in a view:
.transaction { transaction in
transaction.animation = nil
}
This cuts animation propagation at that point in the tree.
Extremely useful for:
- list updates
- pagination inserts
- layout recalculations
- accessibility updates
🧵 Nested Animations: Who Wins?
withAnimation(.spring()) {
stateA.toggle()
withAnimation(.linear) {
stateB.toggle()
}
}
SwiftUI resolves this by transaction priority:
- inner transactions override outer ones
- but only for the state they mutate
Result:
-
stateA→ spring -
stateB→ linear
Transactions are merged, not replaced.
⚠️ Why Some Animations “Don’t Run”
Common causes:
1. No animatable difference
SwiftUI only animates changes between animatable values.
if isOn {
ViewA()
} else {
ViewB()
}
Without transitions, this is a replacement, not an animation.
2. Identity changed
If a view’s identity changes:
- SwiftUI treats it as a new view
- animation has nothing to interpolate
This is why .id() often breaks animations.
3. Transaction was disabled upstream
A parent may have:
.transaction { $0.animation = nil }
Which cancels animations downstream.
🔁 Animation Interruptions
SwiftUI animations are interruptible by design.
If state changes again:
- the current animation is interrupted
- SwiftUI interpolates from the current presentation value
- not the original value
This is why SwiftUI animations feel smooth even under rapid updates.
🎛️ Transactions vs animation(_:)
Key difference:
-
.animation()→ attaches behavior to a view -
Transaction→ controls the update pipeline
Prefer transactions when:
- coordinating multiple updates
- disabling animations selectively
- working with lists
- handling pagination
- managing accessibility motion
♿ Reduce Motion & Transactions
When “Reduce Motion” is enabled:
SwiftUI automatically modifies transactions:
- reduces or removes animations
- switches to opacity changes
- shortens durations
If you hard-code animations without respecting transactions,
you can accidentally break accessibility.
🧠 Mental Model Cheat Sheet
- State changes create transactions
- Transactions carry animation intent
- Views read transactions, not animation modifiers
- Identity determines whether animation can occur
- Nested transactions merge intelligently
- Disabling animations is a transaction concern
- Interruptions are expected and correct
🚀 Final Thoughts
SwiftUI animation is not magic — it’s transaction-driven state propagation.
Once you understand transactions:
- animations become predictable
- bugs become explainable
- performance improves
- interruptions feel intentional
- advanced UI becomes manageable
Top comments (0)