DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Transactions & Update Propagation

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()
}
Enter fullscreen mode Exit fullscreen mode

Does two things:

  1. Mutates state
  2. 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)
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 }
    }
}
Enter fullscreen mode Exit fullscreen mode

Text("B") does not animate — even though its parent does.


⚖️ Transaction vs .animation(_:value:)

This modifier:

.animation(.easeInOut, value: isExpanded)
Enter fullscreen mode Exit fullscreen mode

Creates a scoped implicit transaction.

Differences:

  • .animation(value:) is declarative
  • withAnimation is imperative

Use:

  • .animation(value:) for simple view-driven animation
  • withAnimation for 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
}
Enter fullscreen mode Exit fullscreen mode

To animate async results:

await MainActor.run {
    withAnimation {
        isLoaded = true
    }
}
Enter fullscreen mode Exit fullscreen mode

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)