swiftui · essay · architecture · state

Data Flows Down, Actions Flow Up

Published February 23, 2026 · 2 min read · beginner

The confusion

SwiftUI code often starts clean, then drifts into mixed ownership:

  • child views mutating data they do not own
  • screens passing too many bindings
  • side effects scattered across the tree

Everything still compiles, but reasoning gets harder with each feature.


What SwiftUI is actually doing

SwiftUI rebuilds view descriptions from current state.

That makes two directions explicit:

  • data and configuration move from parent to child
  • user intent moves from child back to parent

If both directions are modeled clearly, updates stay predictable.


The mental model

Data flows down. Actions flow up.

Downward flow:

  • immutable values
  • read/write bindings when ownership stays above

Upward flow:

  • closures describing intent (onDelete, onSave, onToggle)

Bindings are not ownership transfer. They are controlled write access.


A small proof

import SwiftUI

struct CounterScreen: View {
    @State private var count = 0

    var body: some View {
        CounterRow(
            count: count,
            onIncrement: { count += 1 }
        )
    }
}

struct CounterRow: View {
    let count: Int
    let onIncrement: () -> Void

    var body: some View {
        HStack {
            Text("Count: \(count)")
            Spacer()
            Button("Increment", action: onIncrement)
        }
    }
}

CounterScreen owns the state, CounterRow renders data and forwards intent.


Why this matters in real apps

  • Performance: state changes stay local and intentional
  • Architecture: ownership remains visible in each API
  • Readability: you can scan a view and see who owns what

This pattern also makes refactoring safer: extracted views stay dumb by default.


Where this model breaks down

It is not a strict law.

Cases that need adaptation:

  • very deep trees where dependency injection via environment is cleaner
  • local ephemeral state (focus, temporary UI flags) inside leaf views
  • advanced list row editing where value + callback can become verbose

Even then, ownership should still be explicit.


One sentence to remember

If a child view decides behavior, pass an action; if it reflects state, pass data.

Next steps