SwiftUI updates don’t just “happen”.
Every state change flows through a transaction — and understanding that flow is what separates:
- smooth, predictable animations
- from glitchy, half-animated UI
- from views updating “too much”
- from state changes not animating at all
Most SwiftUI developers use transactions without realizing it.
This post explains what transactions are, how updates propagate, and how SwiftUI decides what animates, what re-renders, and what doesn’t — using modern SwiftUI patterns.
🧠 What Is a SwiftUI Transaction?
A Transaction is a container that carries metadata about a state change.
It includes:
- whether animations are enabled
- what animation to use
- whether updates should be immediate
- context for view updates
Every time state changes, SwiftUI creates a transaction.
Even when you don’t write one.
🔄 The Update Propagation Pipeline
When state changes:
State mutation
↓
Transaction created
↓
View invalidation
↓
Body recomputation
↓
Layout pass
↓
Diffing
↓
Rendering (with or without animation)
Transactions flow down the view tree.
Children inherit transaction context from parents.
🎬 Implicit Animations = Transactions
This code:
withAnimation(.easeInOut) {
isExpanded.toggle()
}
Does two things:
- Mutates state
- Wraps that mutation in a transaction with animation metadata
SwiftUI then:
- propagates that transaction
- animates any animatable values affected by the change
No animation code inside views — just data flow.
⚠️ Why Some Views Animate and Others Don’t
Only values that:
- changed inside the same transaction
- are animatable
- and are diffed as changed
will animate.
Example:
Text("Hello")
.opacity(isVisible ? 1 : 0)
If isVisible changes outside an animation transaction → no animation.
🧩 The Transaction Type (Explicit Control)
You can intercept or modify transactions:
.transaction { tx in
tx.animation = .spring()
}
This lets you:
- override parent animations
- disable animations for subtrees
- enforce consistent motion
🚫 Disabling Animations Selectively
Sometimes you don’t want animations.
.transaction { tx in
tx.disablesAnimations = true
}
Common use cases:
- initial layout
- list updates
- pagination inserts
- state restoration
- performance-critical paths
This prevents animation noise.
🔁 Nested Transactions (Who Wins?)
Rules:
- inner transactions override outer ones
- closest transaction wins
- no transaction = inherits parent
Example:
withAnimation(.easeIn) {
VStack {
Text("A")
Text("B")
.transaction { $0.animation = nil }
}
}
Text("B") does not animate — even though its parent does.
⚖️ Transaction vs .animation(_:value:)
This modifier:
.animation(.easeInOut, value: isExpanded)
Creates a scoped implicit transaction.
Differences:
-
.animation(value:)is declarative -
withAnimationis imperative
Use:
-
.animation(value:)for simple view-driven animation -
withAnimationfor event-driven state changes
🧠 Update Propagation Rules (Critical)
SwiftUI propagates updates:
- top-down through the hierarchy
- only to views that depend on changed state
- stopping at unchanged identity boundaries
That means:
- sibling views don’t update unless needed
- children update only if inputs changed
- transactions don’t magically animate everything
This is why clean state ownership matters.
🧵 Async Updates & Transactions
Async updates do not automatically animate.
Task {
await load()
isLoaded = true // no animation
}
To animate async results:
await MainActor.run {
withAnimation {
isLoaded = true
}
}
Transactions must exist at the moment of mutation.
⚠️ Common Bugs Caused by Transaction Misuse
❌ Animations firing multiple times
Cause: repeated state mutations in a loop
❌ Animations not firing
Cause: mutation outside transaction
❌ Animations restarting
Cause: identity reset (not a transaction issue)
❌ Janky list animations
Cause: large diff + implicit animation
Solution:
- disable animations for list updates
- animate only user-driven changes
🧠 Mental Model to Remember
State changes create transactions.
Transactions define animation behavior.
Views simply respond.
SwiftUI animations are not imperative.
They are data-driven side effects of transactions.
🚀 Final Thoughts
Understanding transactions gives you:
- full control over animation behavior
- predictable update propagation
- smoother UI
- better performance
- fewer “why did this animate?” bugs
Top comments (0)