Swift Concurrency
Concurrency lets your app do multiple things at the same time. A common example: your app fetches data from the internet while still responding to taps from the user. Swift 5.5 introduced async/await — a modern, readable way to write concurrent code without the complexity of callbacks and completion handlers.
The Problem: Blocking the Main Thread
Every iOS app has a main thread that drives the UI. If you run a slow task (like a network call) on the main thread, the entire app freezes until it finishes. Users see a hanging screen.
// BAD — blocks the main thread
func loadUser() {
let data = fetchFromServer() // Imagine this takes 3 seconds
updateUI(with: data) // UI is frozen for 3 seconds
}Diagram: Sync vs Async
Synchronous (blocking): Main Thread: [fetch data — 3s wait —][update UI] User sees: [ app frozen ] Asynchronous (non-blocking): Main Thread: [update UI][respond to taps][update UI with data] Background: [fetch data — 3s ————————→ done] User sees: [app stays responsive]
Marking a Function async
Add async to a function's signature to declare that it can suspend and resume without blocking. Inside an async function, use await before any other async call.
func fetchUsername(for id: Int) async -> String {
// Simulates a network delay
try? await Task.sleep(nanoseconds: 1_000_000_000)
return "User_\(id)"
}
// Call it:
Task {
let name = await fetchUsername(for: 42)
print("Fetched: \(name)") // Output: Fetched: User_42
}The Task { } block creates a new concurrent task from synchronous code — like a function or a button tap handler.
async throws — Async + Error Handling
An async function can also throw errors. Combine both modifiers together.
enum APIError: Error {
case badURL
case noData
}
func fetchPost(id: Int) async throws -> String {
guard id > 0 else { throw APIError.badURL }
try await Task.sleep(nanoseconds: 500_000_000)
return "Post content for id \(id)"
}
Task {
do {
let post = try await fetchPost(id: 5)
print(post) // Output: Post content for id 5
} catch {
print("Error: \(error)")
}
}Awaiting Multiple Tasks: async let
Use async let to start multiple async operations at the same time, then collect their results together. Both run in parallel — not one after the other.
func fetchTitle() async -> String {
try? await Task.sleep(nanoseconds: 500_000_000)
return "Swift Guide"
}
func fetchAuthor() async -> String {
try? await Task.sleep(nanoseconds: 300_000_000)
return "Alice"
}
Task {
async let title = fetchTitle() // starts immediately
async let author = fetchAuthor() // starts immediately (parallel)
let t = await title
let a = await author
print("\(t) by \(a)")
// Output: Swift Guide by Alice
// Total wait: ~0.5s (not 0.8s)
}Diagram: Sequential vs Parallel async let
Sequential (await each one): |── fetchTitle (0.5s) ──|── fetchAuthor (0.3s) ──| Total: 0.8s Parallel (async let): |── fetchTitle (0.5s) ──| |── fetchAuthor (0.3s) ──| Total: 0.5s ← faster
Task Groups
A task group runs a dynamic number of async tasks in parallel and collects all their results.
func fetchAllPosts(ids: [Int]) async -> [String] {
await withTaskGroup(of: String.self) { group in
for id in ids {
group.addTask {
return "Post \(id) content"
}
}
var results: [String] = []
for await result in group {
results.append(result)
}
return results
}
}
Task {
let posts = await fetchAllPosts(ids: [1, 2, 3])
print(posts)
}MainActor — Updating the UI Safely
UI updates must always happen on the main thread. Mark a function or class with @MainActor to guarantee it runs there.
@MainActor
func updateLabel(text: String) {
print("UI update: \(text)")
}
Task {
let data = await fetchUsername(for: 1)
await updateLabel(text: data) // safely back on main thread
}Actor — Thread-Safe Shared State
An actor is like a class but protects its properties from simultaneous access by multiple tasks. Only one task can access an actor's internals at a time.
actor BankAccount {
private var balance: Double = 0
func deposit(amount: Double) {
balance += amount
}
func getBalance() -> Double {
return balance
}
}
let account = BankAccount()
Task { await account.deposit(amount: 100) }
Task { await account.deposit(amount: 50) }
Task {
let total = await account.getBalance()
print("Balance: \(total)")
}Structured vs Unstructured Concurrency
| Type | Tool | Lifetime |
|---|---|---|
| Structured | async let, TaskGroup | Tied to the enclosing scope |
| Unstructured | Task { } | Lives independently |
| Detached | Task.detached { } | No parent, no actor inheritance |
Summary
Swift concurrency with async/await keeps your app responsive by running slow work off the main thread in readable, linear-looking code. Use async let to run independent tasks in parallel and collect results faster. Use @MainActor to pin UI updates to the main thread. Use actor to protect shared mutable state from race conditions. Together, these tools replace messy callback chains with clean, safe concurrent code.
