swift · essay · concurrency · architecture

Concurrency Boundaries in SwiftUI Screens

Published September 28, 2026 · 3 min read · advanced

The confusion

SwiftUI makes async work feel easy until the screen starts doing too much. Then people mix task creation, state mutation, and rendering into the same place and wonder why the code feels fragile.

The problem is not concurrency itself. The problem is that the boundary between UI and async work gets blurry.

What SwiftUI is actually doing

SwiftUI redraws views frequently. That means any async work tied to a view must be designed carefully so it can start, stop, and restart without causing duplicate side effects.

The screen boundary is the right place to manage that work. The body is not.

In practice, that means:

  • the view describes the UI state
  • .task starts work when the view is present
  • the model or service owns long-lived async behavior
  • cancellation is handled intentionally

The mental model

Concurrency belongs to the feature boundary, not the rendering boundary.

If the work needs lifecycle, retries, or cancellation, it probably belongs somewhere more explicit than body.

This matters because SwiftUI can rebuild views at any time. Your code should remain stable even when the rendering layer is not.

A small proof

import SwiftUI

struct SyncScreen: View {
    @State private var message = "Idle"

    var body: some View {
        Text(message)
            .task {
                message = "Loading..."
                await Task.yield()
                message = "Done"
            }
    }
}

This works, but it also shows the boundary clearly: the view can trigger the work, while the work itself stays small and explicit. If the task grew larger, it would be a sign to move the async logic into a dedicated model or service.

Why this matters in real apps

Concurrency bugs are often ownership bugs in disguise. If a row starts a task, who cancels it? If a screen starts a task, what happens when the screen disappears? If a service does the work, how does the UI receive updates?

Those are architecture questions, not syntax questions.

The cleaner the boundary, the easier it is to answer them.

Where this model breaks down

Very small async work can stay close to the view if it is easy to reason about and harmless to repeat. But once you introduce fetching, caching, or cancellation, the boundary should become explicit.

That is especially true in scrolling lists, where views appear and disappear constantly.

One sentence to remember

Treat concurrency as feature logic with a lifecycle, not as a side effect of rendering.

Next steps

These follow-ups keep the same boundary discipline across concurrency and side effects: