Migrating callback based code to Swift Concurrency with continuations
Published on: April 24, 2022Swift's async/await feature significantly enhances the readability of asynchronous code for iOS 13 and later versions. For new projects, it enables us to craft more expressive and easily understandable asynchronous code, which closely resembles synchronous code. However, adopting async/await may require substantial modifications in existing codebases, especially if their asynchronous API relies heavily on completion handler functions.
Fortunately, Swift offers built-in mechanisms that allow us to create a lightweight wrapper around traditional asynchronous code, facilitating its transition into the async/await paradigm. In this post, I'll demonstrate how to convert callback-based asynchronous code into functions compatible with async/await, using Swift's async
keyword.
Converting a Callback-Based Function to Async/Await
Callback-based functions vary in structure, but typically resemble the following example:
func validToken(_ completion: @escaping (Result<Token, Error>) -> Void) {
// Function body...
}
The validToken(_:)
function above is a simplified example, taking a completion closure and using it at various points to return the outcome of fetching a valid token.
Tip: To understand more about
@escaping
closures, check out this post on @escaping in Swift.
To adapt our validToken
function for async/await, we create an async throws
version returning a Token
. The method signature becomes cleaner:
func validToken() async throws -> Token {
// ...
}
The challenge lies in integrating the existing callback-based validToken
with our new async version. We achieve this through a feature known as continuations. Swift provides several types of continuations:
withCheckedThrowingContinuation
withCheckedContinuation
withUnsafeThrowingContinuation
withUnsafeContinuation
These continuations exist in checked and unsafe variants, and in throwing and non-throwing forms. For an in-depth comparison, refer to my post that compares checked and unsafe in great detail.
Here’s the revised validToken
function using a checked continuation:
func validToken() async throws -> Token {
return try await withCheckedThrowingContinuation { continuation in
// Implementation...
}
}
This function uses withCheckedThrowingContinuation
to bridge our callback-based validToken
with the async version. The continuation
object, created within the function, must be used to resume execution, or the method will remain indefinitely suspended.
The callback-based validToken
is invoked immediately, and once resume
is called, the async validToken
resumes execution. Since the Result
type can contain an Error
, we handle both success and failure cases accordingly.
The Swift team has simplified this pattern by introducing a version of resume
that accepts a Result
object:
func validToken() async throws -> Token {
return try await withCheckedThrowingContinuation { continuation in
validToken { result in
continuation.resume(with: result)
}
}
}
This approach is more streamlined and elegant.
Remember two crucial points when working with continuations:
- A continuation can only be resumed once.
- It's your responsibility to call
resume
within the continuation closure; failure to do so will leave the function awaiting indefinitely.
Despite minor differences (like error handling), all four with*Continuation
functions follow these same fundamental rules.
Summary
This post illustrated how to transform a callback-based function into an async function using continuations. You learned about the different types of continuations and their applications.
Continuations are an excellent tool for gradually integrating async/await into your existing codebase without a complete overhaul. I've personally used them to transition large code segments into async/await gradually, allowing for intermediate layers that support async/await in, say, view models and networking, without needing a full rewrite upfront.
In conclusion, continuations offer a straightforward and elegant solution for converting existing callback-based functions into async/await compatible ones.