Concurrency
Swift offers built-in tools for writing asynchronous and parallel code in a clear, structured manner. Asynchronous code can pause and resume, but only one part runs at once. This allows your app to handle quick tasks, like UI updates, while working on slower ones, such as loading data from the internet or processing files. Parallel code runs multiple operations simultaneously—for instance, a multi-core device can execute several tasks across its cores. Programs often combine these for concurrency, handling multiple tasks while pausing those waiting on external resources.
Concurrency adds flexibility but increases complexity. You can't predict which code runs together or in what order, risking data races—when multiple parts access shared changeable data simultaneously. Swift's concurrency features detect and prevent most data races at compile time, with runtime checks halting others. Use actors and isolation, covered later, to safeguard data.
Note: If you're familiar with threads, Swift's concurrency builds on them but hides direct interaction. Async functions can yield threads, allowing others to run without guaranteeing resumption on the same thread.
Without Swift's concurrency tools, code can be hard to follow. For example, this fetches a list of image names, grabs the first image, and displays it:
fetchImages(fromAlbum: "Family Trip") { imageNames in
let sortedNames = imageNames.sorted()
let name = sortedNames[0]
loadImage(named: name) { image in
display(image)
}
}Nested closures make complex code messy.
Defining and Calling Asynchronous Functions
An asynchronous function or method can pause mid-execution. Unlike synchronous ones, which complete, error, or loop forever, async ones do the same but can suspend while waiting.
Mark a function async with async after parameters (before -> for returns). For throwing async functions, place async before throws.
Example: Fetching image names from an album:
func fetchImages(fromAlbum name: String) async -> [String] {
let result = // ... async network code ...
return result
}Call async functions with await to mark suspension points—similar to try for errors. Execution pauses only on async calls; it's explicit, not automatic.
Example: Fetch and display the first image:
let imageNames = await fetchImages(fromAlbum: "Family Trip")
let sortedNames = imageNames.sorted()
let name = sortedNames[0]
let image = await loadImage(named: name)
display(image)Possible execution flow:
- Run to first
await, callfetchImages, suspend. - Other code runs (e.g., background tasks).
- Resume after return, assign to
imageNames. - Sort synchronously (no
await). - Suspend on
loadImage. - Resume, display image.
Async code requires specific contexts: async bodies, @main static methods, or unstructured tasks (later).
Use Task.sleep(for:) for testing delays:
func fetchImages(fromAlbum name: String) async throws -> [String] {
try await Task.sleep(for: .seconds(2))
return ["IMG001", "IMG99", "IMG0404"]
}Call with try await. Async functions resemble throwing ones but can't be wrapped synchronously—convert top-down when adding concurrency.
Asynchronous Sequences
Instead of awaiting full collections, use asynchronous sequences to handle elements one-by-one.
Example: Reading input lines async:
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}Use for try await for potential suspensions per iteration. Conform to AsyncSequence like Sequence for sync loops.
Calling Asynchronous Functions in Parallel
Sequential awaits run one-at-a-time:
let firstImage = await loadImage(named: imageNames[0])
let secondImage = await loadImage(named: imageNames[1])
let thirdImage = await loadImage(named: imageNames[2])
let images = [firstImage, secondImage, thirdImage]
display(images)For parallelism, use async let:
async let firstImage = loadImage(named: imageNames[0])
async let secondImage = loadImage(named: imageNames[1])
async let thirdImage = loadImage(named: imageNames[2])
let images = await [firstImage, secondImage, thirdImage]
display(images)Starts without waiting; awaits results later. Mix with sequential awaits as needed.
Tasks and Task Groups
A task is async work. All async code runs in tasks, which can run concurrently but sequentially inside.
async let creates implicit child tasks. For dynamic control, use TaskGroup:
await withTaskGroup(of: Data.self) { group in
let imageNames = await fetchImages(fromAlbum: "Family Trip")
for name in imageNames {
group.addTask {
await loadImage(named: name)
}
}
for await image in group {
display(image)
}
}Creates children for each image; displays as ready (order unpredictable).
For results:
let images = await withTaskGroup(of: Data.self) { group in
let imageNames = await fetchImages(fromAlbum: "Family Trip")
for name in imageNames {
group.addTask {
await loadImage(named: name)
}
}
var results: [Data] = []
for await image in group {
results.append(image)
}
return results
}Structured concurrency ensures parents wait for children, escalates priorities, propagates cancellation, and shares values.
Task Cancellation
Tasks check cancellation cooperatively (e.g., throw CancellationError, return nil).
Use addTaskUnlessCancelled, check Task.isCancelled:
let images = await withTaskGroup { group in
let imageNames = await fetchImages(fromAlbum: "Family Trip")
for name in imageNames {
let added = group.addTaskUnlessCancelled {
Task.isCancelled ? nil : await loadImage(named: name)
}
guard added else { break }
}
var results: [Data] = []
for await image in group {
if let image { results.append(image) }
}
return results
}Handles partial results on cancel.
For immediate notification:
let task = await Task.withTaskCancellationHandler {
// ...
} onCancel: {
print("Canceled!")
}
// Later...
task.cancel() // Prints "Canceled!"Unstructured Concurrency
Unstructured tasks lack parents. Use Task { ... } (inherits context) or Task.detached { ... } (independent):
let newImage = // ... image data ...
let handle = Task {
await add(newImage, toAlbum: "Spring Outing")
}
let result = await handle.valueManage manually for correctness.
Isolation
Protect shared mutable data via data isolation: immutable data, single-task references, or actor-protected access.
The Main Actor
The main actor safeguards UI data. Pre-concurrency, all runs on it; move heavy work off.
Mark with @MainActor:
@MainActor
func display(_: Data) {
// ... UI display code ...
}Call with await off-main:
func loadAndDisplayImage(named name: String) async {
let image = await loadImage(named: name)
await display(image)
}For closures: @MainActor in { ... }.
For types:
@MainActor
struct ImageAlbum {
var imageNames: [String]
func renderUI() { /* ... UI code ... */ }
}Or per-property/method for granularity.
Frameworks often imply @MainActor via protocols/classes/wrappers.
Actors
Define custom actors for safe sharing:
actor TempRecorder {
let name: String
var readings: [Int]
private(set) var highest: Int
init(name: String, reading: Int) {
self.name = name
self.readings = [reading]
self.highest = reading
}
}Access with await:
let recorder = TempRecorder(name: "Outdoor", reading: 25)
print(await recorder.highest) // "25"Inside actor, no await needed:
extension TempRecorder {
func add(with reading: Int) {
readings.append(reading)
if reading > highest {
highest = reading
}
}
}Prevents races by serializing access. Direct external writes error.
Synchronous methods ensure no suspensions, protecting invariants:
extension TempRecorder {
func convertToCelsius() {
for i in readings.indices {
readings[i] = (readings[i] - 32) * 5 / 9
}
}
}Global Actors
Main actor is a @globalActor singleton. Define custom ones similarly.
Sendable Types
Shareable types conform to Sendable. Ways: value types with sendable state, immutable types, or safely managed mutable state.
Example:
struct TempValue: Sendable {
var value: Int
}
extension TempRecorder {
func record(from value: TempValue) {
readings.append(value.value)
}
}
let recorder = TempRecorder(name: "Tea Pot", reading: 85)
let value = TempValue(value: 45)
await recorder.record(from: value)Implicit for non-public structs. Mark unavailable to suppress:
struct FileHandle {
let id: Int
}
@available(*, unavailable)
extension FileHandle: Sendable {}