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:
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:
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:
withCheckedContinuation
or its throwing variantwithCheckedThrowingContinuation
withUnsafeContinuation
or its throwing variantwithUnsafeThrowingContinuation
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:
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:
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
:
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:
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:
-
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).
-
Don't Discard the Continuation: Make sure your code always calls
resume
on the continuation in all code paths. -
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:
// 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:
// 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:
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:
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:
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:
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:
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:
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:
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:
- Resume the continuation exactly once
- Make sure to resume the continuation in all code paths
- 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
- Convert a completion handler-based networking function to use async/await
- Wrap the
URLSession.shared.dataTask
method directly using continuations - Create an async wrapper for a function that uses a delegate pattern instead of completion handlers
- Practice error handling with continuations by wrapping a function that can fail in multiple ways
- 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! :)