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.
