Swift Concurrency

Concurrency means doing multiple things at the same time. In apps, this matters because slow tasks — like downloading a file or fetching data from a server — should not freeze the user interface. Swift 5.5 introduced a built-in concurrency model using async and await that makes writing concurrent code as readable as regular code.

The Restaurant Kitchen Analogy


┌──────────────────────────────────────────────────┐
│  Without concurrency (sequential):               │
│  Chef makes starter → waits → then main course   │
│  → waits → then dessert. Takes forever.          │
│                                                  │
│  With concurrency (parallel):                    │
│  Chef A makes starter                            │
│  Chef B makes main course   (all at once)        │
│  Chef C makes dessert                            │
│                                                  │
│  async/await = coordinating multiple chefs       │
└──────────────────────────────────────────────────┘

The Problem Without Concurrency

// This blocks the entire app while fetching
func fetchData() {
    let data = downloadFromServer()   // app freezes here
    display(data)
}

When a slow operation runs on the main thread, the entire app freezes — buttons stop responding and animations stall. Users see a dead screen.

async – Mark a Function as Asynchronous

func fetchUserName() async -> String {
    // Imagine this takes 2 seconds
    return "Riya"
}

The async keyword marks a function that can pause and resume. While it is paused (waiting for a result), other work continues on the thread.

await – Wait for an Async Result

func showProfile() async {
    let name = await fetchUserName()
    print("Welcome, \(name)!")
}

await pauses execution at that line until the async function returns. The thread is not blocked — other tasks run while this one waits.

Task – Run Async Code

Task {
    let name = await fetchUserName()
    print("User: \(name)")
}

A Task creates a new unit of async work. Use it to call async functions from synchronous contexts like viewDidLoad or a button tap handler.

async/await Flow Diagram


┌──────────────────────────────────────────────────┐
│  showProfile() starts                            │
│       │                                          │
│  await fetchUserName()  ← pauses here            │
│       │                                          │
│  [App keeps running other work]                  │
│       │                                          │
│  fetchUserName() returns "Riya"                  │
│       │                                          │
│  print("Welcome, Riya!")  ← resumes here         │
└──────────────────────────────────────────────────┘

Calling Multiple Async Functions Sequentially

func loadDashboard() async {
    let user = await fetchUserName()
    let posts = await fetchPostCount()
    print("\(user) has \(posts) posts")
}

Each await runs one after another. This is clean but not the fastest approach when the two results do not depend on each other.

Running Async Tasks in Parallel with async let

func loadDashboard() async {
    async let user  = fetchUserName()
    async let posts = fetchPostCount()

    let (name, count) = await (user, posts)
    print("\(name) has \(count) posts")
}

async let starts both tasks immediately without waiting for the first to finish. The await at the end collects both results together. This can cut load time significantly when tasks are independent.

Handling Errors in Async Functions

enum NetworkError: Error {
    case noConnection
}

func fetchData() async throws -> String {
    throw NetworkError.noConnection
}

Task {
    do {
        let data = try await fetchData()
        print(data)
    } catch {
        print("Failed: \(error)")
    }
}
// Failed: noConnection

Async functions combine with the error handling system naturally. Use async throws in the signature and try await at the call site.

MainActor – Update UI Safely

@MainActor
func updateLabel(text: String) {
    label.text = text   // always runs on the main thread
}

UI updates must happen on the main thread. Marking a function or class with @MainActor guarantees it always runs there, no matter which thread calls it.

Actors – Thread-Safe Shared State

actor BankAccount {
    var balance: Double = 0

    func deposit(amount: Double) {
        balance += amount
    }
}

let account = BankAccount()

Task { await account.deposit(amount: 500) }
Task { await account.deposit(amount: 300) }

An actor protects its data from simultaneous access by multiple threads. Only one piece of code accesses the actor's data at a time, eliminating race conditions without manual locking.

Leave a Comment

Your email address will not be published. Required fields are marked *