DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Contract-First Integration: Building Scalable Systems With Flyway, OpenAPI, and Kafka
  • Building a Zero-Cost Approval Workflow With AWS Lambda Durable Functions
  • Programmatic Brand Extraction: Pulling Logos, Colors, and Assets from Any URL
  • You Don't Get to Retrofit Trust: Why API Security Must Be Designed In, Not Bolted On

Trending

  • From Indicators to Insights: Automating IOC Enrichment Using Python and Threat Feeds
  • LLM Agents and Getting Started with Them
  • Designing API-First EMR Architectures in .NET: Enabling Modular Growth in Compliance-Driven Systems
  • Architecting an Embedded Efficiency Layer: A Platform Deep Dive into Day-Two Operational Tuning
  1. DZone
  2. Coding
  3. Languages
  4. Swift Concurrency, Part 3: Bridging Legacy APIs With Continuations

Swift Concurrency, Part 3: Bridging Legacy APIs With Continuations

Swift Continuations: the essential bridge between legacy callback-based APIs and modern async/await. Wrap completion handlers and delegates into clean, linear code.

By 
Nikita Vasilev user avatar
Nikita Vasilev
·
Apr. 13, 26 · Analysis
Likes (1)
Comment
Save
Tweet
Share
2.0K Views

Join the DZone community and get the full member experience.

Join For Free

Swift concurrency has fundamentally changed how we write asynchronous code, making it more readable and safer.

However, the real world is still full of legacy APIs and SDKs that rely on completion handlers and delegates. You cannot simply rewrite every library overnight. This is where Continuations come in. They act as a powerful bridge, allowing us to wrap older asynchronous patterns into modern async functions, ensuring that our codebases remain clean and consistent even when dealing with legacy code.

The Challenge of Traditional Async Patterns

For years, iOS developers relied on two fundamental approaches for asynchronous operations: completion closures and delegate callbacks. Consider a typical network request using completion handlers:

Swift
 
func fetchUserData(completion: @escaping (User?, Error?) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        // Handle response in a different scope
        if let error = error {
            completion(nil, error)
            return
        }
        // Process data...
        completion(user, nil)
    }.resume()


Similarly, delegate patterns scatter logic across multiple methods:

Swift
 
class LocationManager: NSObject, CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, 
                        didUpdateLocations locations: [CLLocation]) {
        // Handle success in one method
    }
    
    func locationManager(_ manager: CLLocationManager, 
                        didFailWithError error: Error) {
        // Handle failure in another method
    }
}


Both approaches share a critical weakness: they fragment your program’s control flow. Instead of reading code from top to bottom, developers must mentally jump between closures, delegate methods, and completion callbacks. This cognitive overhead breeds subtle bugs-forgetting to invoke a completion handler, calling it multiple times, or losing track of error paths through nested callbacks.

Bridging the Gap With Async/Await

Continuations transform these fragmented patterns into linear, readable code. They provide the missing link between callback-based APIs and Swift’s structured concurrency model. By wrapping legacy asynchronous operations, you can write code that suspends at natural points and resumes when results arrive, without modifying the underlying implementation.

Here’s the transformation in action. Our callback-based network function becomes:

Swift
 
func fetchUserData() async throws -> User {
    try await withCheckedThrowingContinuation { continuation in
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                continuation.resume(throwing: error)
                return
            }
            // Process and resume with result
            continuation.resume(returning: user)
        }.resume()
    }
}


Now calling code flows naturally:

Swift
 
do {
    let user = try await fetchUserData()
    let profile = try await fetchProfile(for: user)
    updateUI(with: profile)
} catch {
    showError(error)
}


Understanding Continuation Mechanics

A continuation represents a frozen moment in your program’s execution. When you mark a suspension point with await, Swift doesn’t simply pause and wait; it captures the entire execution context into a lightweight continuation object. This includes local variables, the program counter, and the call stack state.

This design enables Swift’s runtime to operate efficiently. Rather than dedicating one thread per asynchronous operation (the traditional approach that leads to thread explosion), the concurrency system maintains a thread pool sized to match your CPU cores. When a task suspends, its thread becomes available for other work. When the task is ready to resume, the runtime uses any available thread to reconstruct the execution state from the continuation.

Consider what happens during a network call:

Swift
 
func processData() async throws {
    let config = loadConfiguration()  // Runs immediately
    let data = try await downloadData()  // Suspends here
    let result = transform(data, with: config)  // Resumes here
    return result
}


At the await point, Swift creates a continuation capturing config and the program location. The current thread is freed for other tasks. When downloadData() completes, the runtime schedules resumption—but not necessarily on the same thread. The continuation ensures all local state travels with the execution, making thread switching transparent.

Manual Continuation Creation

Swift provides two continuation variants, each addressing different needs:

  • CheckedContinuation performs runtime validation, detecting common errors like resuming twice or forgetting to resume. This safety net makes it the default choice during development:
Swift
 
func getCurrentLocation() async throws -> CLLocation {
    try await withCheckedThrowingContinuation { continuation in
        let manager = CLLocationManager()
        manager.requestLocation()
        
        manager.locationHandler = { locations in
            if let location = locations.first {
                continuation.resume(returning: location)
            }
        }
        
        manager.errorHandler = { error in
            continuation.resume(throwing: error)
        }
    }
}


If you accidentally resume twice, you’ll see a runtime warning: SWIFT TASK CONTINUATION MISUSE: continuation resumed multiple times.

  • UnsafeContinuation removes these checks for maximum performance. Use it only in hot paths where profiling confirms the overhead matters, and you’ve thoroughly verified correctness:
Swift
 
func criticalOperation() async -> Result {
    await withUnsafeContinuation { continuation in
        performHighFrequencyCallback { result in
            continuation.resume(returning: result)
        }
    }
}


Working With Continuation Resume Methods

The continuation API enforces a strict contract: resume exactly once. This guarantee prevents resource leaks and ensures predictable execution. Swift provides four resume methods to cover different scenarios:

  • resume() for operations without return values:
Swift
 
func waitForAnimation() async {
    await withCheckedContinuation { continuation in
        UIView.animate(withDuration: 0.3, animations: {
            self.view.alpha = 0
        }) { _ in
            continuation.resume()
        }
    }
}


  • resume(returning:) to provide a result:
Swift
 
func promptUser(message: String) async -> Bool {
    await withCheckedContinuation { continuation in
        let alert = UIAlertController(title: message, message: nil, preferredStyle: .alert)
        
        alert.addAction(UIAlertAction(title: "Yes", style: .default) { _ in
            continuation.resume(returning: true)
        })
        
        alert.addAction(UIAlertAction(title: "No", style: .cancel) { _ in
            continuation.resume(returning: false)
        })
        
        present(alert, animated: true)
    }
}


  • resume(throwing:) for error propagation:
Swift
 
func authenticateUser() async throws -> User {
    try await withCheckedThrowingContinuation { continuation in
        authService.login { result in
            switch result {
            case .success(let user):
                continuation.resume(returning: user)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}


  • resume(with:) as a convenient shorthand for Result types:
Swift
 
func loadImage(from url: URL) async throws -> UIImage {
    try await withCheckedThrowingContinuation { continuation in
        imageLoader.fetch(url) { result in
            continuation.resume(with: result)
        }
    }
}


Practical Integration Patterns

When migrating real-world code, certain patterns emerge repeatedly. Here’s how to handle a delegate-based API with multiple possible outcomes:

Swift
 
class NotificationPermissionManager: NSObject, UNUserNotificationCenterDelegate {
    func requestPermission() async throws -> Bool {
        try await withCheckedThrowingContinuation { continuation in
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: granted)
                }
            }
        }
    }
}


For callbacks that might never fire (like user cancellation), ensure you handle all paths:

Swift
 
func selectPhoto() async -> UIImage? {
    await withCheckedContinuation { continuation in
        let picker = UIImagePickerController()
        
        picker.didSelect = { image in
            continuation.resume(returning: image)
        }
        
        picker.didCancel = {
            continuation.resume(returning: nil)
        }
        
        present(picker, animated: true)
    }
}


Conclusion

Continuations represent more than a compatibility layer; they embody Swift’s pragmatic approach to evolution. By providing clean integration between legacy and modern patterns, they enable gradual migration rather than forcing disruptive rewrites. As you encounter older APIs in your codebase, continuations offer a path forward that maintains both backward compatibility and forward-looking code quality.

The safety guarantees of CheckedContinuation make experimentation low-risk, while UnsafeContinuation provides an escape hatch for proven, performance-critical code. Master these tools, and you’ll find that even the most callback-laden legacy code can integrate seamlessly into modern async workflows.

API Swift (programming language)

Published at DZone with permission of Nikita Vasilev. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Contract-First Integration: Building Scalable Systems With Flyway, OpenAPI, and Kafka
  • Building a Zero-Cost Approval Workflow With AWS Lambda Durable Functions
  • Programmatic Brand Extraction: Pulling Logos, Colors, and Assets from Any URL
  • You Don't Get to Retrofit Trust: Why API Security Must Be Designed In, Not Bolted On

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook