SwiftUI animations look effortless — until they don’t.
That’s when you see:
- animations restarting randomly
- views snapping instead of animating
- animations not firing at all
- conflicting animations fighting each other
- performance dropping during transitions
The reason is simple:
SwiftUI animations are state-driven, transactional, and identity-sensitive.
This post explains how SwiftUI animations actually work under the hood so you can design animations that are smooth, predictable, and performant.
🧠 The Core Rule of SwiftUI Animation
SwiftUI does not animate views.
It animates state changes.
State changes
↓
SwiftUI computes differences
↓
If an animation is attached → interpolate values
If state doesn’t change → nothing animates.
🔄 Implicit vs Explicit Animations
Implicit Animation
Attached directly to a view:
.opacity(show ? 1 : 0)
.animation(.easeInOut, value: show)
- Animates when
showchanges - Clean and declarative
- Preferred for most UI
Explicit Animation
Wrap state changes:
withAnimation(.spring()) {
show.toggle()
}
- Animates all animatable changes inside the block
- Powerful but broader in scope
- Use carefully
🧾 Transactions: The Hidden Animation Container
Every SwiftUI update runs inside a Transaction.
A transaction defines:
- animation
- animation speed
- whether animations are disabled
You can access it:
.transaction { txn in
txn.animation = .easeOut
}
Or disable animations:
.transaction { txn in
txn.disablesAnimations = true
}
This is how SwiftUI decides what animates together.
⚠️ Why Animations Sometimes Don’t Run
Common causes:
- state changes outside the animation’s
value - view identity changed
-
id()reset - view removed & reinserted
- conditional rendering
- animation attached to the wrong level
Example bug:
if show {
MyView()
}
When show becomes false:
- view is removed
- no animation possible
Better:
MyView()
.opacity(show ? 1 : 0)
🆔 Identity & Animation Reset
Animations reset when identity changes.
This forces a fresh view:
MyView()
.id(UUID()) // ❌ resets animation every update
Even subtle identity changes (like unstable list IDs) will:
- restart animations
- break transitions
- cause flickering
📌 Stable identity = stable animations
🔀 Animatable Properties (What Can Animate?)
SwiftUI animates values that conform to VectorArithmetic:
DoubleCGFloatColorCGSizeCGPointAngle
These animate smoothly:
.offset(x: dragX)
.scaleEffect(scale)
.rotationEffect(angle)
Non-animatable changes will jump instantly.
🧩 Custom Animations with Animatable
For advanced control:
struct MorphView: View, Animatable {
var progress: CGFloat
var animatableData: CGFloat {
get { progress }
set { progress = newValue }
}
var body: some View {
Rectangle()
.scaleEffect(1 + progress)
}
}
SwiftUI interpolates animatableData automatically.
This is how:
- shape morphing
- waveform animations
- custom loaders
- progress indicators
are built.
🧵 Animation Timing & Interruptions
SwiftUI animations are interruptible by default.
If state changes mid-animation:
- SwiftUI retargets the animation
- no snapping
- no restart
Example:
withAnimation(.spring()) {
offset = newValue
}
Changing offset again smoothly redirects the animation.
This is why SwiftUI animations feel “alive”.
🔁 Multiple Animations in One View
Bad pattern:
.animation(.easeIn, value: a)
.animation(.easeOut, value: b)
Only the last animation wins.
Correct pattern:
.animation(.easeIn, value: a)
.animation(.spring(), value: b)
Attached at the correct view level, not stacked blindly.
📦 Animations & Lists
In lists:
- identity matters even more
- unstable IDs break transitions
- insertions/deletions animate by default
Use:
withAnimation {
items.append(newItem)
}
And ensure:
ForEach(items, id: \.id)
Avoid heavy animations inside rows.
⚡ Performance Rules for Animations
✔ Animate transforms (opacity, scale, offset)
✔ Avoid animating layout where possible
✔ Keep animations short
✔ Avoid deep animated hierarchies
✔ Avoid animating large lists
✔ Prefer implicit animations
Animations are cheap — layout recalculations are not.
🧠 Debugging Animation Bugs
Ask:
- Did state change?
- Is the animation attached to the right value?
- Did identity change?
- Is the view conditionally removed?
- Is a parent invalidating identity?
- Is layout fighting animation?
99% of animation bugs fall into these.
🧠 Mental Model Cheat Sheet
State change
↓
Transaction created
↓
SwiftUI diffs values
↓
Animatable values interpolate
↓
Layout + render
Control state → control animation.
🚀 Final Thoughts
SwiftUI animations are not magic.
They are:
- state-driven
- identity-sensitive
- transactional
- interruptible
- predictable
Once you understand the internals:
- animations stop breaking
- transitions feel intentional
- performance stays smooth
- your UI feels truly native
Top comments (0)