Swift introduced async/await in Swift 5.5, bringing a modern and streamlined approach to handle asynchronous code. It replaces the old callback-based patterns and makes code easier to read, maintain, and debug.
In this tutorial, we’ll explore how async/await works in Swift and walk through practical examples to help you get started as this is by far one of the most important things to learn as a iOS developer.
Why Async/Await?
Before async/await, asynchronous operations in Swift often used closures or completion handlers:
func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
DispatchQueue.global().async {
// Simulate network call
sleep(2)
completion(.success("Data fetched"))
}
}
fetchData { result in
switch result {
case .success(let data):
print(data)
case .failure(let error):
print(error)
}
}
While this works, it can quickly become messy when chaining multiple asynchronous calls (the so-called “callback hell”). Async/await solves this by making asynchronous code look and behave like synchronous code.
Basic Example with Async/Await
Here’s how the above example looks using async/await:
func fetchData() async throws -> String {
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // Simulate network delay
return "Data fetched"
}
Task {
do {
let data = try await fetchData()
print(data)
} catch {
print("Error: \(error)")
}
}
Notice how clean and linear the flow is compared to the completion handler version.
Making a Network Request with URLSession
Let’s build a real-world example: fetching data from an API.
struct Post: Decodable {
let id: Int
let title: String
}
func fetchPost() async throws -> Post {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
let (data, _) = try await URLSession.shared.data(from: url)
let post = try JSONDecoder().decode(Post.self, from: data)
return post
}
Task {
do {
let post = try await fetchPost()
print("Post title: \(post.title)")
} catch {
print("Failed to fetch post: \(error)")
}
}
This code fetches a post from a placeholder API and decodes it into a Post struct. Thanks to async/await, the code is readable and avoids nested closures.
Using Async/Await in SwiftUI
Async/await integrates beautifully with SwiftUI. Here’s a simple view that loads data when it appears:
import SwiftUI
struct ContentView: View {
@State private var postTitle = "Loading..."
var body: some View {
Text(postTitle)
.task {
do {
let post = try await fetchPost()
postTitle = post.title
} catch {
postTitle = "Error loading post"
}
}
}
}
The .task modifier runs asynchronous code when the view appears, making it perfect for loading data without cluttering your view logic.
Error Handling and Cancellation
With async/await, error handling is done using try and catch. You can also cancel tasks using Task:
let task = Task {
try await fetchData()
}
// To cancel the task later:
task.cancel()
This provides fine-grained control over asynchronous operations.
Conclusion
Swift’s async/await simplifies asynchronous programming by making code more linear, readable, and less error-prone. Whether you’re fetching data, handling animations, or managing tasks, modern concurrency in Swift helps you write better code.
Have you tried using async/await in your projects? Share your experiences and tips in the comments!
