Migrating callback based code to Swift Concurrency with continuations

Published on: April 24, 2022

Swift'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:

  1. A continuation can only be resumed once.
  2. 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.

Subscribe to my newsletter