Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
32: async/await in Swift
Imagine you're at a busy coffee shop. You walk up to the counter, order a vanilla latte, and pay. Now, you have two options. You could stand exactly where you are, staring at the barista, refusing to move or speak to anyone until that latte is in your hand. That's "synchronous" execution—and it's a great way to create a massive line of angry customers behind you. Or, you can take a buzzer, step aside, check your emails, and chat with a friend. You're still "waiting" for the coffee, but you aren't blocking the entire shop from functioning. When the buzzer goes off, you step back up and grab your drink.
In Swift, async/await is that buzzer. It allows your code to pause a heavy task (like downloading a 10MB image) without freezing your entire app's user interface. The thread is freed up to do other things, and once the task finishes, your code picks up exactly where it left off.
Moving Past the "Pyramid of Doom"
Before async/await, we relied on completion handlers—closures that get called when a task finishes. If you had to fetch a user, then fetch their posts, then fetch the comments for those posts, you ended up with code that drifted further and further to the right of your screen. I call this the "Pyramid of Doom." It's hard to read, and it's even harder to handle errors without forgetting to call the completion block in one of your guard statements.
With async/await, that nested mess becomes a linear list of instructions. It looks like synchronous code, but it behaves asynchronously.
Defining and Calling Asynchronous Work
To tell Swift that a function might pause, you mark it with the async keyword. When you call that function, you use await to mark the spot where the execution might suspend.
// This function is marked as async because it fetches data from a server
func fetchUserProfile(userId: String) async throws -> UserProfile {
let url = URL(string: "https://api.example.com/users/\(userId)")!
// We 'await' the network response. The thread is freed here.
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(UserProfile.self, from: data)
}
Notice the try await. Since network calls can fail, we combine error handling with the suspension point. I've found that this makes the "happy path" of your logic much easier to follow because you aren't hunting through nested braces to see where the error is actually handled.
Orchestrating Multiple Calls
The real magic happens when you have dependent tasks. Let's say you need a user's profile before you can fetch their specific recent photos. In the old way, this was a nightmare of nested closures. Now, it's just this:
func loadDashboardData(userId: String) async {
do {
// Step 1: Get the profile
let profile = try await fetchUserProfile(userId: userId)
// Step 2: Use the profile to get the photos
let photos = try await fetchUserPhotos(userId: profile.id)
print("Loaded \(photos.count) photos for \(profile.name)")
} catch {
print("Something went wrong: \(error)")
}
}
The code reads top-to-bottom. It's clean. It's predictable. But there's one catch: you can't call an async function from a standard, synchronous function (like viewDidLoad in UIKit or a button action) without a bridge.
Bridging the Gap with Task
When you're in a synchronous environment but need to kick off some asynchronous work, you use a Task. Think of a Task as a wrapper that creates a new asynchronous context.
func handleRefreshButtonTapped() {
// We are in a sync function, so we wrap the async call in a Task
Task {
do {
await loadDashboardData(userId: "123")
print("Refresh complete!")
} catch {
// Handle the error on the main thread
}
}
}
I usually recommend keeping your Task blocks as small as possible. Don't put your entire business logic inside the Task; instead, use the Task to call a dedicated async function. It keeps your code testable and prevents your view controllers from becoming dumping grounds for logic.
📋 Practical Task
Refactoring the Movie Detail Fetcher
You've inherited a piece of legacy code that uses completion handlers to fetch movie details and cast lists. This code is currently a "Pyramid of Doom" and is very difficult to maintain. Your goal is to refactor these functions to use async/await.
Current Legacy Code:
struct Movie { let title: String; let id: Int }
struct Actor { let name: String }
func fetchMovie(id: Int, completion: @escaping (Result<Movie, Error>) -> Void) {
// Simulated network delay
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
completion(.success(Movie(title: "Inception", id: id)))
}
}
func fetchCast(movieId: Int, completion: @escaping (Result<[Actor], Error>) -> Void) {
// Simulated network delay
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
completion(.success([Actor(name: "Leonardo DiCaprio"), Actor(name: "Elliot Page")]))
}
}
// The "Pyramid of Doom" call site
func loadMoviePage() {
fetchMovie(id: 101) { movieResult in
switch movieResult {
case .success(let movie):
fetchCast(movieId: movie.id) { castResult in
switch castResult {
case .success(let cast):
print("Movie: \(movie.title), Cast: \(cast.map { $0.name })")
case .failure(let error):
print("Cast error: \(error)")
}
}
case .failure(let error):
print("Movie error: \(error)")
}
}
}
Your Task:
- Rewrite
fetchMovie(id:)andfetchCast(movieId:)to beasync throwsfunctions (you can replace theDispatchQueuedelay withtry await Task.sleep(nanoseconds: 1_000_000_000)). - Rewrite the
loadMoviePage()function to use aTaskblock andawaitboth calls linearly. - Ensure all errors are handled using a
do-catchblock.
There are no comments for now.