Swift Concurrency
async/await Basics
// Mark async functions with async; call them with await
func loadUser(id: UUID) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// Call from sync context via Task
Task {
do {
let user = try await loadUser(id: currentID)
await MainActor.run { self.user = user }
} catch {
await MainActor.run { self.errorMessage = error.localizedDescription }
}
}
Structured Concurrency with TaskGroup
func loadFeed(userIDs: [UUID]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in userIDs {
group.addTask { try await loadUser(id: id) }
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
async let — Parallel Independent Operations
func loadProfile(id: UUID) async throws -> Profile {
async let user = loadUser(id: id)
async let posts = loadPosts(userID: id)
async let friends = loadFriends(userID: id)
// All three start concurrently; await collects results
return Profile(
user: try await user,
posts: try await posts,
friends: try await friends
)
}
Actors
Actors protect mutable state from concurrent access.
actor Cache<Key: Hashable, Value> {
private var store: [Key: Value] = [:]
func get(_ key: Key) -> Value? { store[key] }
func set(_ key: Key, value: Value) { store[key] = value }
func remove(_ key: Key) { store.removeValue(forKey: key) }
}
// Accessing actor state requires await
let cache = Cache<String, Data>()
await cache.set("key", value: data)
let cached = await cache.get("key")
@MainActor
Annotate types or methods that must run on the main thread.
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
func load() async {
isLoading = true
defer { isLoading = false }
items = try! await fetchItems() // already on MainActor
}
}
// Or annotate individual methods
class DataService {
@MainActor func updateUI(with data: [Item]) {
// safe to touch UIKit/SwiftUI state here
}
}
AsyncSequence
// Consuming an AsyncSequence
for await line in url.lines { // URLSession.lines is an AsyncSequence
processLine(line)
}
// Custom AsyncSequence via AsyncStream
func timerStream(interval: Duration) -> AsyncStream<Date> {
AsyncStream { continuation in
let timer = Timer.scheduledTimer(withTimeInterval: interval.timeInterval, repeats: true) { _ in
continuation.yield(Date())
}
continuation.onTermination = { _ in timer.invalidate() }
}
}
for await tick in timerStream(interval: .seconds(1)) {
print("Tick: \(tick)")
}
Task Cancellation
func fetchWithCancellation(url: URL) async throws -> Data {
try Task.checkCancellation() // throws CancellationError if cancelled
let (data, _) = try await URLSession.shared.data(from: url)
try Task.checkCancellation() // check again after suspension
return data
}
// Cancel from the outside
let task = Task { try await fetchWithCancellation(url: url) }
task.cancel() // cooperative cancellation
Sendable
// Value types are Sendable by default (struct, enum with Sendable storage)
struct Message: Sendable {
let id: UUID
let text: String
}
// Classes need explicit conformance or @unchecked
final class Config: @unchecked Sendable {
private let lock = NSLock()
private var _value: Int = 0
var value: Int {
get { lock.withLock { _value } }
set { lock.withLock { _value = newValue } }
}
}
Common Anti-Patterns
DispatchQueue.main.asyncin async code — useawait MainActor.run { }or@MainActor- Unstructured tasks everywhere — prefer
async letandTaskGroupfor structured concurrency - Not checking cancellation — call
Task.checkCancellation()at suspension points - Calling actor methods with
DispatchQueue— actors replace queues; don't mix @unchecked Sendablewithout proper locking — audit every instance