Understanding Swift Concurrency: Async/Await Explained
Understanding Swift Concurrency: Async/Await Explained
Swift 5.5 introduced a revolutionary approach to handling asynchronous code. The new async/await syntax makes concurrent code more readable and safer than traditional completion handlers.
The Problem with Completion Handlers
Before async/await, asynchronous code often led to callback hell:
func fetchUserData(completion: @escaping (Result<User, Error>) -> Void) {
fetchUserID { result in
switch result {
case .success(let id):
fetchUserProfile(id: id) { result in
switch result {
case .success(let profile):
fetchUserPosts(userID: id) { result in
// More nesting...
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
}
Enter Async/Await
The same code with async/await is dramatically simpler:
func fetchUserData() async throws -> User {
let id = try await fetchUserID()
let profile = try await fetchUserProfile(id: id)
let posts = try await fetchUserPosts(userID: id)
return User(id: id, profile: profile, posts: posts)
}
Key Concepts
Async Functions
Mark functions as async when they perform asynchronous work:
func downloadImage(from url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
Await Keyword
The await keyword marks suspension points where the function can pause:
let image = await downloadImage(from: imageURL)
// Execution pauses here until the image is downloaded
print("Image downloaded!")
Task and Structured Concurrency
Use Task to create concurrent work:
Task {
do {
let user = try await fetchUserData()
print("User loaded: \(user.name)")
} catch {
print("Error: \(error)")
}
}
Actors for Thread Safety
Actors protect shared mutable state:
actor UserCache {
private var cache: [String: User] = [:]
func user(for id: String) -> User? {
return cache[id]
}
func setUser(_ user: User, for id: String) {
cache[id] = user
}
}
let cache = UserCache()
await cache.setUser(user, for: "123")
Parallel Execution
Run multiple operations concurrently:
async let profile = fetchProfile()
async let posts = fetchPosts()
async let followers = fetchFollowers()
let (userProfile, userPosts, userFollowers) = await (profile, posts, followers)
Task Groups
For dynamic concurrency:
func fetchAllImages(urls: [URL]) async throws -> [UIImage] {
try await withThrowingTaskGroup(of: UIImage.self) { group in
for url in urls {
group.addTask {
try await downloadImage(from: url)
}
}
var images: [UIImage] = []
for try await image in group {
images.append(image)
}
return images
}
}
Best Practices
- Don't block threads - Use async/await instead of semaphores
- Use actors for shared mutable state
- Structure your concurrency with task groups
- Handle cancellation properly with
Task.isCancelled - Use MainActor for UI updates
@MainActor
func updateUI(with data: String) {
label.text = data
}
Conclusion
Swift's concurrency model makes asynchronous code safer and more maintainable. Start using async/await in your projects today!