Continuations for interacting asynchronous tasks with synchronous code

Swift has introduced new features that help us adapt older completionhandler-style APIs to modern asynchronous code.

For example, this function returns its values ​​asynchronously using a completion handler:

import Foundation

func fetchLatestNews(completion: @escaping ([String]) -> Void) {
    DispatchQueue.main.async {
        completion(["Swift 5.5 release", "Apple acquires Apollo"])
    }
}

If we wanted to use it with async/await, we could rewrite the function, but there are various reasons why this might not be possible – for example, it might be taken from an external library.

Continuations allow us to wrap a completion handler and async functions so we can wrap older code in a more modern API. For example, the withCheckedContinuation() function creates a new continuation that can run any code we want, and then calls resume(returning:) to send the value back, even if it is part of the completion handler.

So we can create a second asynchronous fetchLatestNews() function, wrapped around the old function with completionhandler:

func fetchLatestNews() async -> [String] {
    await withCheckedContinuation { continuation in
        fetchLatestNews { items in
            continuation.resume(returning: items)
        }
    }
}

Now we can get our original functionality in an async function like this:

func printNews() async {
    let items = await fetchLatestNews()

    for item in items {
        print(item)
    }
}

The term “checked” continuation means that Swift performs checks at runtime on our behalf: are we calling resume() once and only once? This is important because if we never call resume() there will be a resource leak, but if we call it twice we will most likely run into problems.

Important: We must call resume() exactly once.

Because checking our continuations comes with cost and performance at runtime, Swift also provides a function withUnsafeContinuation(), which works exactly the same except it doesn't perform any checks at runtime. This means that Swift won't warn us if we forget to call resume(), and if we call it twice, the behavior will be undefined.

Since these two functions are called in the same way, we can easily switch between them. It seems most likely that people will use withCheckedContinuation() when writing their functions, so Swift will issue warnings and even crash if continuations are used incorrectly, but some may then switch to withUnsafeContinuation() if cost and performance are critical to them during execution of checked continuations.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *