Skip to content

Concurrency

Concurrency allows your application to perform multiple tasks at the same time. This keeps your program fast and responsive even while it handles heavy workloads in the background.

Async / Await

Modern Swift uses async and await keywords to handle asynchronous tasks. This allows you to write complex background code that reads cleanly from top to bottom, exactly like standard code.

A standard synchronous function cannot pause its execution, so it cannot directly call an asynchronous function. You must use a Task { } block to create a brand new concurrent environment, allowing you to safely bridge the gap between your standard sequential code and your async operations.

Additionally, the Task type itself provides built-in utilities for managing concurrency, such as Task.sleep, which simply pauses the current async operation without freezing the rest of your app.

swift
func fetchData() async -> String {
    // Using the built-in utility to pause this specific async operation for 1 second
    try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
    return "Data received"
}

// 'Task' creates a concurrent environment to run the async code
Task {
    let data = await fetchData()
    print(data)
}

Structured Concurrency (async let)

You can use async let to run multiple background tasks in parallel. This saves a massive amount of time by executing them simultaneously and then waiting for all of them to finish before moving forward.

swift
async let first = fetchData()
async let second = fetchData()

// Await both tasks to finish at the exact same time and store in an array
let results = await [first, second]

You can easily capture the final results into separate variables using a tuple.

swift
// Await both tasks to finish at the exact same time and assign to separate variables using a tuple
let (firstResult, secondResult) = await (first, second)
print(firstResult, secondResult)

Actors

Actors are reference types that behave similarly to standard classes but are specifically designed to protect their state from data races. They ensure that only one single task can access or modify their properties at a time, completely preventing overlapping changes that cause mysterious crashes.

If an actor has a method or property that does not touch mutable state, you can mark it with the nonisolated keyword. This tells the compiler it is perfectly safe to access from anywhere without using await.

swift
actor BankAccount {
    var balance = 0
    
    func deposit(amount: Int) {
        balance += amount
    }
    
    // Safe to call without 'await' because it doesn't touch mutable state
    nonisolated func getBankName() -> String {
        return "Swift Community Bank"
    }
}

let account = BankAccount()

// We can call this synchronously without 'await'
let name = account.getBankName() 

Task {
    // Accessing mutable state requires 'await'
    await account.deposit(amount: 100)
}

@MainActor

This is a specialized actor that guarantees your code runs strictly on the main thread. You must use this whenever you are updating the visual user interface to ensure the screen refreshes safely.

swift
@MainActor
func updateUI() {
    // Update labels, buttons, etc.
}

@MainActor on Classes

You can apply @MainActor to an entire class to guarantee all of its properties and methods run on the main thread. If that class has a specific function doing heavy background work, you can opt that single function out using nonisolated.

swift
@MainActor
class ProfileViewModel {
    var username = "Guest"
    
    func updateName(newName: String) {
        username = newName // Safely updates on the main thread
    }
    
    // Opts out of the MainActor requirement to do background work
    nonisolated func performHeavyCalculation() {
        // Do heavy math here without freezing the screen
    }
}

Using MainActor.run

When you are deep inside a background Task and suddenly need to update the interface, you do not need a whole class. You can just hop over to the main thread briefly using MainActor.run.

swift
Task {
    let data = await fetchData() // Running in the background
    
    // Explicitly hop over to the main thread just for this specific block
    await MainActor.run {
        print("Update UI with \(data)") 
    }
}

Sendable Types

When passing data between different tasks or actors, that data must be safe to share. Swift uses the Sendable protocol to mark types that can be safely transferred across concurrent boundaries.

Value types like Structs and Enums are implicitly sendable because they are copied when passed around, meaning no two tasks are ever fighting over the exact same data in memory. Standard classes are generally not sendable because they share the same reference in memory, which risks dangerous data races.

swift
// A struct that's safe to share across threads — it's a value type
// Note: Structs are Implicitly Sendable
struct UserProfile: Sendable {
    let name: String
    let score: Int
}

actor ProfileCache {
    // Safe to return from an actor — UserProfile is Sendable
    func getProfile() -> UserProfile {
        return UserProfile(name: "Chris", score: 9800)
    }
}