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
68: Continuations for Bridging Callback APIs
A few years ago, I was helping a teammate migrate a massive legacy codebase to Swift's new concurrency model. He had this one specific service—a custom network wrapper written back in the Swift 3 days—that used nested completion handlers for everything. He'd spent the whole morning trying to wrap it in a Task block, but he ended up with this "pyramid of doom" inside his async functions, defeating the whole purpose of using await. He looked at me, genuinely frustrated, and asked why he still had to deal with callbacks when he was already inside an asynchronous context.
The problem is that async/await is a language feature, but many of the libraries we use (including some older Apple frameworks) were written long before it existed. You can't just put await in front of a function that takes a closure; the compiler will just stare at you blankly. To fix this, we use "continuations." Think of a continuation as a way to tell Swift: "Pause this function right here, go do some work in this old callback-style API, and let me know when you're done so I can resume where I left off."
Turning Callbacks into Awaitable Calls
The most common tool for this is withCheckedContinuation. When you call this function, Swift suspends the current task and hands you a continuation object. Your job is to pass that object into your legacy callback and call resume(returning:) exactly once when the data comes back.
// Imagine this is some old SDK you can't change
struct LegacyImageDownloader {
func fetchImage(url: URL, completion: @escaping (UIImage?) -> Void) {
// Old school networking...
}
}
// Here is how we bridge it
extension LegacyImageDownloader {
func fetchImage(url: URL) async -> UIImage? {
await withCheckedContinuation { continuation in
// We call the old API and use the callback to resume the async function
self.fetchImage(url: url) { image in
continuation.resume(returning: image)
}
}
}
}
Notice how the calling code now looks clean: let image = await downloader.fetchImage(url: url). The complexity is hidden inside the wrapper. I usually recommend adding these wrappers in an extension so you don't pollute the original service logic.
Dealing with Errors and the "Exactly Once" Rule
Most real-world APIs don't just return a value; they return a Result or an error. In those cases, you use withCheckedThrowingContinuation. This version allows you to call resume(throwing:) if something goes wrong, which seamlessly integrates with Swift's do-catch blocks.
func fetchUserAccount(id: String) async throws -> Account {
try await withCheckedThrowingContinuation { continuation in
legacyAPI.getUser(id: id) { result in
switch result {
case .success(let account):
continuation.resume(returning: account)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
Now, here is the part where you have to be careful. The "Checked" part of withCheckedContinuation is there for a reason. Swift enforces a strict rule: you must resume the continuation exactly once. If you call resume twice, your app will crash. If you never call it (perhaps because of a guard statement that returns early without resuming), the task will hang forever, leaking memory. I've seen this happen in production more times than I'd like to admit—usually because a developer forgot to call resume inside an error branch.
📋 Practical Task
Exercise: Converting the Legacy Location Service to Async/Await
You have been handed a LegacyLocationManager that uses a completion handler to fetch the user's current coordinates. Your task is to create an async wrapper for it so it can be used in a modern Swift concurrency pipeline.
import Foundation
struct Coordinates {
let latitude: Double
let longitude: Double
}
enum LocationError: Error {
case permissionDenied
case timeout
}
// DO NOT MODIFY THIS CLASS
class LegacyLocationManager {
func getCurrentLocation(completion: @escaping (Result<Coordinates, LocationError>) -> Void) {
// Simulating a network delay
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
let success = Bool.random()
if success {
completion(.success(Coordinates(latitude: 34.0522, longitude: -118.2437)))
} else {
completion(.failure(.timeout))
}
}
}
}
// YOUR TASK:
// 1. Create an extension on LegacyLocationManager.
// 2. Implement an async function called 'fetchLocation()' that returns Coordinates.
// 3. Use withCheckedThrowingContinuation to bridge the callback to async/await.
extension LegacyLocationManager {
// Write your code here
}
// Test your implementation:
Task {
let manager = LegacyLocationManager()
do {
let coords = try await manager.fetchLocation()
print("Location found: \(coords.latitude), \(coords.longitude)")
} catch {
print("Error fetching location: \(error)")
}
}There are no comments for now.