Concurrency in Swift: async/await, Tasks & Actors
iOS Development — Architecture & Data
Chapter 2 · Concurrency in Swift: async/await, Tasks & Actors
Fundamentals Chapter 9 covered async/await just enough to fetch one real thing.
A ViewModel doing genuine work needs more: running several async operations at once, protecting shared
mutable state from real data races, and guaranteeing UI-touching code actually runs on the main thread.
Unstructured Work: Task {}
Fundamentals' own .task {} view modifier is tied directly to a view's own lifecycle. A
ViewModel method — not itself a view — needs a different real starting point:
Task { }, which launches a genuine, independent unit of async work
immediately, not bound to any one view's own appear/disappear cycle.
.task {}, a plain Task { } isn't automatically cancelled by anything —
Fundamentals Chapter 9's own real safety guarantee doesn't apply here for free. A long-lived ViewModel
method that starts background work this way is genuinely responsible for its own cancellation if that's
ever needed (Course 3's own deployment chapters return to this).
Structured, Parallel Work: async let
await-ing two calls back to back runs them sequentially — the second
doesn't start until the first finishes. async let is real, genuine structured concurrency:
both operations start immediately, running in parallel.
loadDashboard() is cancelled while both are still running, real, automatic cancellation
propagates to both, with no manual bookkeeping needed. This is what "structured" means in structured
concurrency: child work can never outlive its own parent scope.
Actors: Protecting Mutable State from Real Data Races
Introduced alongside async/await in Swift 5.5, an
actor is a real reference type — like a class — that the compiler guarantees only ever
executes one piece of its own code at a time, even when called from multiple concurrent tasks at once.
class with a shared, mutable var count — called concurrently from
several real tasks at once — genuinely risks a data race: two increments reading the same starting value
before either writes back, silently losing an update. An actor's own serialized access makes
that specific class of bug structurally impossible, enforced by the compiler, not just careful coding.
Calling an actor's own method or property from outside the actor requires
await, even though nothing about increment() itself looks asynchronous — the
await here reflects real, potential waiting for the actor's own serialized turn, not network
latency.
@MainActor: Guaranteeing the Main Thread
UIKit and SwiftUI both genuinely require UI updates to happen on the main thread — a real, hard rule, not
a suggestion. @MainActor is Swift's own real, compiler-enforced way of guaranteeing that.
@Observable state — state SwiftUI reads to redraw the UI — from a
background thread is real, genuinely undefined behavior: at best a visual glitch, at worst a crash.
Marking a UI-facing ViewModel @MainActor makes the compiler itself enforce that every one
of its own property writes happens on the main thread, closing off that entire class of bug at compile
time rather than leaving it to be discovered at runtime.
Hands-On Exercises
Write a function loadProfile() using async let to fetch a user's real name and avatar image URL in parallel (two separate, imagined async functions), then await both together and return a combined result.
Define a real actor called DownloadTracker with a private(set) var activeDownloads = 0 and two methods, start() and finish(), incrementing and decrementing it. Explain, in your own words, why calling tracker.activeDownloads from outside the actor requires await.
Explain, in your own words, why marking a ViewModel @MainActor is specifically about where code runs, while marking a type an actor is specifically about serializing access to shared state — and why a typical SwiftUI ViewModel usually needs the former rather than the latter.
Chapter 2 Quick Reference
Task { }— unstructured async work, not tied to a view's lifecycle, and not automatically cancelled the way.task {}isasync let— real, structured, parallel concurrency; child work can't outlive its own parent scopeactor(Swift 5.5, 2021) — a reference type with compiler-enforced serialized access, preventing real data races on shared mutable state- Calling an actor's own members from outside it requires
await, reflecting a real wait for serialized access, not necessarily network latency @MainActor— compiler-enforced guarantee that a type's own code runs on the main thread, essential for any ViewModel updating UI-facing state