Asynchronous Programming in Swift: Complete Guide with Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction to Asynchronous Programming in Swift

Modern mobile and desktop applications in Swift (iOS/macOS) increasingly require performing long-running operations — loading data from the network, working with files, processing images — without blocking the user interface. Until recently, developers used closures (completion handlers) and Grand Central Dispatch (GCD), which led to the so-called "callback hell" and complicated code readability. With the release of Swift 5.5, Apple introduced a fundamentally new asynchronous programming model based on the keywords async and await, as well as actors and Task structures. In this guide, we will break down all the key concepts with practical examples.



1. Basics of async/await in Swift

The keyword async indicates that a function or method can suspend its execution to wait for the result of an asynchronous operation. A call to such a function is marked with the word await. This allows writing asynchronous code as linearly as synchronous code.



// Declaring an asynchronous functionfunc fetchUserData(from url: URL) async throws -> Data {    let (data, _) = try await URLSession.shared.data(from: url)    return data}

// Using it in a task contextTask { do { let url = URL(string: "https://api.example.com/user")! let userData = try await fetchUserData(from: url) print("Data received: \\(userData.count) bytes") } catch { print("Loading error: \\(error.localizedDescription)") }}


Note: the await call suspends the current task but does not block the thread. The system itself switches context when the result is ready, making the code efficient and safe.



2. Actors — safe state management

Asynchrony introduces the problem of data races. To solve this, Swift introduced actors. An actor is a reference type that isolates its state and guarantees that only one task can access its mutable properties at any given time.



actor Counter {    private var value = 0        func increment() {        value += 1    }        func getValue() -> Int {        return value    }}

// Using an actorlet counter = Counter()

Task { await counter.increment() let currentValue = await counter.getValue() print("Current value: \\(currentValue)")}


The keyword actor automatically makes all methods and properties (except immutable ones) asynchronous for external access. This helps avoid locks and deadlocks typical of manual lock management (NSLock, DispatchSemaphore).



3. TaskGroup and parallel task execution

When you need to execute several independent asynchronous operations in parallel, use TaskGroup. This is a structure that allows launching child tasks and collecting their results.



func fetchMultipleUsers(ids: [Int]) async throws -> [User] {    try await withThrowingTaskGroup(of: User.self) { group in        var users: [User] = []                for id in ids {            group.addTask {                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.0)            }        }                for try await user in group {            users.append(user)        }        return users    }}

// UsageTask { let users = try? await fetchMultipleUsers(ids: [1, 2, 3, 4, 5]) print("Users loaded: \\(users?.count ?? 0)")}


The method withThrowi

Blogs

Book Recommendations