Skip to main content

Swift Continuation

Introduction

Swift's async/await pattern provides a clean and straightforward way to write concurrent code. However, not all APIs follow this modern approach. Many existing frameworks and libraries, especially those written before Swift 5.5, use completion handlers (callbacks) to handle asynchronous operations. This is where Swift Continuation steps in.

Swift Continuation serves as a bridge between traditional callback-based code and the new async/await concurrency model. It allows you to wrap existing asynchronous code that uses completion handlers into functions that support async/await, making your code more readable and maintainable.

Understanding the Problem

Before we dive into continuations, let's understand the problem they solve. Consider this traditional callback-based function:

swift
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
// Perform some asynchronous network request
URLSession.shared.dataTask(with: URL(string: "https://example.com")!) { data, response, error in
if let error = error {
completion(.failure(error))
return
}

guard let data = data else {
completion(.failure(NSError(domain: "NoDataError", code: 1, userInfo: nil)))
return
}

completion(.success(data))
}.resume()
}

To use this function, you have to provide a completion handler:

swift
fetchData { result in
switch result {
case .success(let data):
print("Data received: \(data.count) bytes")
case .failure(let error):
print("Error: \(error)")
}
}

While this works, it can quickly lead to "callback hell" when you need to chain multiple asynchronous operations. With Swift Continuation, we can transform this callback-based code into a function that works with async/await.

Using Continuations in Swift

Swift offers two main types of continuations:

  1. withCheckedContinuation or its throwing variant withCheckedThrowingContinuation
  2. withUnsafeContinuation or its throwing variant withUnsafeThrowingContinuation

Let's start with the safer "checked" version, which is recommended for most use cases.

Using withCheckedThrowingContinuation

The withCheckedThrowingContinuation function creates a continuation that can be resumed exactly once, and it will produce a runtime error if this rule is violated:

swift
func fetchDataAsync() async throws -> Data {
return try await withCheckedThrowingContinuation { continuation in
fetchData { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}

Now we can use this function with async/await:

swift
do {
let data = try await fetchDataAsync()
print("Data received: \(data.count) bytes")
} catch {
print("Error: \(error)")
}

Non-throwing Continuation

If your API doesn't throw errors, you can use withCheckedContinuation:

swift
func delayedMessage(seconds: Double) async -> String {
return await withCheckedContinuation { continuation in
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
continuation.resume(returning: "Message after \(seconds) seconds")
}
}
}

Usage:

swift
Task {
let message = await delayedMessage(seconds: 2)
print(message) // Output: "Message after 2.0 seconds"
}

Important Rules for Continuations

When working with continuations, several important rules must be followed:

  1. Resume Exactly Once: A continuation must be resumed exactly once. Not resuming will cause your program to hang, and resuming multiple times will cause undefined behavior (and with checked continuations, a runtime error).

  2. Don't Discard the Continuation: Make sure your code always calls resume on the continuation in all code paths.

  3. Don't Let It Escape: Don't store the continuation for later use outside of the callback.

Common Mistakes to Avoid

⚠️ Resuming a continuation multiple times:

swift
// INCORRECT USAGE
func buggyFetchData() async throws -> Data {
return try await withCheckedThrowingContinuation { continuation in
fetchData { result in
if let data = result.success {
continuation.resume(returning: data)
}

if let error = result.failure {
// Error! We might resume twice if both conditions are met
continuation.resume(throwing: error)
}
}
}
}

⚠️ Not resuming a continuation in all code paths:

swift
// INCORRECT USAGE
func buggyFetchData() async throws -> Data {
return try await withCheckedThrowingContinuation { continuation in
fetchData { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure:
// We forgot to resume with an error!
// This will cause the program to hang
}
}
}
}

Unsafe Continuations

Swift also provides unsafe continuations (withUnsafeContinuation and withUnsafeThrowingContinuation), which offer better performance but fewer safety checks:

swift
func fetchDataAsyncUnsafe() async throws -> Data {
return try await withUnsafeThrowingContinuation { continuation in
fetchData { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}

Use unsafe continuations only when you're confident that your code handles the continuation correctly and you need the performance improvement.

Real-World Examples

Example 1: Wrapping UIKit Animation

Let's convert a UIKit animation function to use async/await:

swift
extension UIView {
static func animateAsync(withDuration duration: TimeInterval, animations: @escaping () -> Void) async -> Bool {
await withCheckedContinuation { continuation in
UIView.animate(withDuration: duration, animations: animations) { completed in
continuation.resume(returning: completed)
}
}
}
}

Usage:

swift
func performAnimation() async {
button.alpha = 0
let completed = await UIView.animateAsync(withDuration: 0.3) {
button.alpha = 1
}

if completed {
print("Animation completed successfully")
}
}

Example 2: Location Permission Request

Wrapping Core Location permission request:

swift
import CoreLocation

class LocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var authorizationContinuation: CheckedContinuation<Bool, Never>?

override init() {
super.init()
manager.delegate = self
}

func requestLocationAuthorization() async -> Bool {
if manager.authorizationStatus != .notDetermined {
return manager.authorizationStatus == .authorizedWhenInUse
}

return await withCheckedContinuation { continuation in
authorizationContinuation = continuation
manager.requestWhenInUseAuthorization()
}
}

func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if let continuation = authorizationContinuation {
continuation.resume(returning: manager.authorizationStatus == .authorizedWhenInUse)
authorizationContinuation = nil
}
}
}

Usage:

swift
let locationManager = LocationManager()
let isAuthorized = await locationManager.requestLocationAuthorization()

if isAuthorized {
print("Location access granted")
} else {
print("Location access denied")
}

Example 3: Reading Data from Disk

Creating an async version of reading data from a file:

swift
func readFileAsync(at path: String) async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global(qos: .background).async {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path))
continuation.resume(returning: data)
} catch {
continuation.resume(throwing: error)
}
}
}
}

Usage:

swift
do {
let data = try await readFileAsync(at: "/path/to/file.txt")
print("Read \(data.count) bytes")
} catch {
print("Failed to read file: \(error)")
}

Summary

Swift Continuations provide a crucial tool for integrating legacy callback-based code with Swift's modern async/await concurrency system. By using continuations, you can:

  • Convert callback-based APIs to async/await style
  • Make asynchronous code more readable and maintainable
  • Avoid "callback hell" in complex asynchronous operations

Remember the key rules when working with continuations:

  1. Resume the continuation exactly once
  2. Make sure to resume the continuation in all code paths
  3. Don't store continuations for later use outside their intended scope

For most cases, use the checked variants (withCheckedContinuation and withCheckedThrowingContinuation) to get helpful runtime checks. Only use unsafe continuations when you're confident in your implementation and need the performance improvement.

Additional Resources

Exercises

  1. Convert a completion handler-based networking function to use async/await
  2. Wrap the URLSession.shared.dataTask method directly using continuations
  3. Create an async wrapper for a function that uses a delegate pattern instead of completion handlers
  4. Practice error handling with continuations by wrapping a function that can fail in multiple ways
  5. Create an async sequence using continuations to represent a series of asynchronous events

Happy coding with Swift Continuations!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)