Crash Courses · PhD
Concurrency: async/await & Actors
Write modern Swift concurrency with async/await, run tasks in parallel with async let, and protect shared state with actors.
22 min read+195 XP on completionCert: Swift Crash Course
Tap any word in the text below to start reading from there.
Concurrency: async/await & Actors
Why async/await?
Before Swift 5.5, async code used completion handlers — nested closures that became impossible to read ("callback hell"). async/await makes async code look and read like synchronous code while remaining non-blocking.
Old way (completion handlers):
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) { ... }
func fetchPosts(for user: User, completion: @escaping ([Post]) -> Void) { ... }
fetchUser(id: 1) { result in
switch result {
case .success(let user):
fetchPosts(for: user) { posts in
// now we're 2 levels deep and it gets worse
}
case .failure(let error): ...
}
}New way (async/await):
let user = try await fetchUser(id: 1)
let posts = await fetchPosts(for: user)
// Linear, readable, no nesting---
Writing async Functions
import Foundation
// Simulating a network request
func fetchWeather(for city: String) async throws -> String {
// await suspends this function without blocking the thread
try await Task.sleep(for: .milliseconds(100)) // simulate 100ms network delay
let conditions = ["Sunny", "Cloudy", "Rainy", "Stormy"]
let temp = Int.random(in: 20...35)
return "\(city): \(conditions.randomElement()!), \(temp)°C"
}
func fetchAllWeather() async {
let cities = ["Kingston", "Miami", "London", "Tokyo"]
// Sequential — waits for each before starting next (slow)
for city in cities {
if let weather = try? await fetchWeather(for: city) {
print(weather)
}
}
}
// Start the async work:
Task {
await fetchAllWeather()
}---
Parallel Execution with async let
func fetchWeatherParallel() async {
// All four requests start simultaneously
async let k = fetchWeather(for: "Kingston")
async let m = fetchWeather(for: "Miami")
async let l = fetchWeather(for: "London")
async let t = fetchWeather(for: "Tokyo")
// await here — all four complete and we collect results
let results = await [
(try? k) ?? "Kingston: unavailable",
(try? m) ?? "Miami: unavailable",
(try? l) ?? "London: unavailable",
(try? t) ?? "Tokyo: unavailable",
]
results.forEach { print($0) }
// All 4 complete in ~100ms (parallel) vs ~400ms (sequential)
}
Task { await fetchWeatherParallel() }---
Actors: Thread-Safe Shared State
// Without actor — data race (two threads read/write simultaneously):
// class UnsafeCounter { var count = 0 } // NOT thread-safe
// With actor — Swift serializes all access:
actor SafeCounter {
private var count = 0
private var history: [String] = []
func increment() {
count += 1
history.append("Incremented to \(count)")
}
func decrement() {
count = max(0, count - 1)
}
func value() -> Int { count }
func getHistory() -> [String] { history }
}
// Using an actor requires await — accessing actor state is async:
let counter = SafeCounter()
Task {
await counter.increment()
await counter.increment()
await counter.increment()
let value = await counter.value()
print("Counter: \(value)") // 3
}---
@MainActor for UI
// In a real iOS app, UI updates must happen on the main thread
@MainActor
class ViewModel: ObservableObject {
@Published var items: [String] = []
@Published var isLoading = false
@Published var errorMessage: String?
func loadItems() async {
isLoading = true
defer { isLoading = false } // defer + async work together
do {
// fetch from network (runs on background thread internally)
let fetched = try await fetchFromServer()
items = fetched // safe — we're on MainActor
} catch {
errorMessage = error.localizedDescription
}
}
private func fetchFromServer() async throws -> [String] {
try await Task.sleep(for: .seconds(1)) // simulate network
return ["Item A", "Item B", "Item C"]
}
}---
TaskGroup: Dynamic Concurrency
func fetchMultiple(ids: [Int]) async -> [String] {
await withTaskGroup(of: String?.self) { group in
for id in ids {
group.addTask {
try? await fetchWeather(for: "City\(id)")
}
}
var results: [String] = []
for await result in group {
if let r = result { results.append(r) }
}
return results
}
}
Task {
let results = await fetchMultiple(ids: [1, 2, 3, 4, 5])
print("Got \(results.count) results")
}Reading progress
0% read
In this module
async/await
Task
async let
Actor
0%