Introduction to Asynchronous Programming in Swift
\n\nModern mobile applications rarely go without network requests, database operations, or processing large files. All these operations take time, and if you execute them on the main thread, the application interface will "freeze," and the user will have a poor experience. This is where asynchronous programming comes to the rescue.
\n\nBefore Swift 5.5, developers used closures (completion handlers) or third-party libraries (Combine, RxSwift), which often led to "callback hell" and hard-to-read code. With the release of the new Swift Concurrency model (async/await), writing asynchronous code has become as simple and linear as synchronous code.
\n\nIn this guide, we will break down the key concepts of asynchronous programming in Swift with practical examples. You will learn how to use async/await, Task, Actor, and other tools to create fast and responsive applications for iOS and macOS.
Core Concepts of Swift Concurrency
\n\n1. Async and Await: The Foundation of Asynchrony
\n\nThe keywords async and await are the heart of the new model. async declares a function as asynchronous, and await suspends execution until a result is obtained without blocking the thread.
Let's look at a simple example: loading data from the network.
\n\nimport Foundation\n\n// Declaring an asynchronous function\nfunc fetchUserData() async throws -> String {\n let url = URL(string: "https://api.example.com/user/1")!\n let (data, _) = try await URLSession.shared.data(from: url)\n return String(data: data, encoding: .utf8) ?? "Error"\n}\n\n// Calling an asynchronous function\nTask {\n do {\n let userData = try await fetchUserData()\n print("User data: \\(userData)")\n } catch {\n print("Loading error: \\(error.localizedDescription)")\n }\n}\n\nNote: try await means we are waiting for a result that might throw an error. The code looks like regular synchronous code, but it does not block the main thread.
2. Task: Entry Point into the Async World
\n\nAsynchronous functions can only be called from another asynchronous context. To start an asynchronous operation from synchronous code (e.g., from viewDidLoad()), Task is used.
class ViewController: UIViewController {\n \n override func viewDidLoad() {\n super.viewDidLoad()\n \n // Starting an asynchronous task\n Task {\n let data = await loadData()\n updateUI(with: data)\n }\n }\n \n func loadData() async -> String {\n // Simulating a long load\n try? await Task.sleep(nanoseconds: 2_000_000_000)\n return "Loaded data"\n }\n \n func updateUI(with data: String) {\n print("Updating UI: \\(data)")\n }\n}\n\nImportant: Task creates a new asynchronous task that runs on a background thread. To update the UI, you must return to the main thread — by default, await does this automatically.
3. Actor: Protection Against Data Races
\n\nWhen working with multithreading, the problem of data races often arises, where multiple threads modify the same variable simultaneously. Swift offers an elegant solution — Actor.
Actor is a reference type that guarantees that only one thread can access its mutable state at any given time.
actor Counter {\n private var value = 0\n \n func increment() {\n value += 1\n }\n \n func getValue() -> Int {\n return value\n }\n}\n\n// Usage\nlet counter = Counter