← All posts

Tutorial: Modeling SwiftUI Screen State

A practical walkthrough for replacing scattered booleans with one explicit state model that a real screen can render safely.

SwiftUIArchitectureState

Start with the screen contract

Before writing the view, list the states a user can actually see. A production screen is rarely just data or no data. It can be idle, loading, ready, empty, failed, refreshing or showing stale data. Naming those states first makes the UI easier to reason about.

Create one state enum

A common mistake is spreading state across isLoading, items, errorMessage and hasLoadedOnce. That works until two flags disagree. Prefer one screen state that represents the truth the view should render.

enum OrdersScreenState {
    case idle
    case loading
    case empty
    case loaded([Order])
    case failed(message: String)
}

Render every case explicitly

Now the SwiftUI body becomes a mapping from state to UI. This is the useful discipline: if product adds a retry state or an offline state, the compiler guides the implementation instead of hiding the missing path.

struct OrdersView: View {
    let state: OrdersScreenState

    var body: some View {
        switch state {
        case .idle, .loading:
            ProgressView()
        case .empty:
            EmptyOrdersView()
        case .loaded(let orders):
            OrdersList(orders: orders)
        case .failed(let message):
            ErrorStateView(message: message)
        }
    }
}

Keep local UI state local

Not everything belongs in the feature state. Sheet presentation, focused fields, temporary animation toggles and local selection can stay close to the view. The boundary is product meaning: if product, analytics, deep links or recovery logic care about it, model it deliberately.

The payoff

This pattern makes previews, tests and code reviews better. You can build one preview per state, assert state transitions in the view model, and discuss behavior with product using the same names that appear in code.