Articles, podcasts and news about Swift development, by John Sundell.

Making async system APIs backward compatible

Published on 28 Oct 2021
Discover page available: Concurrency

Although Swift 5.5’s new concurrency system is becoming backward compatible in Xcode 13.2, some of the built-in system APIs that make use of these new concurrency features are still only available on iOS 15, macOS Monterey, and the rest of Apple’s 2021 operating systems.

For example, if we try to use the new async/await-favored version of the URLSession data task API within an app that’s also available on earlier operating system versions, then we’ll get a compiler error:

struct ModelLoader<Model: Decodable> {
    var session = URLSession.shared
    var decoder = JSONDecoder()

    func loadModel(from url: URL) async throws -> Model {
        // Error: 'data(from:delegate:)' is only available in iOS 15.0 or newer
        let (data, _) = try await session.data(from: url)
        let model = try decoder.decode(Model.self, from: data)
        return model
    }
}

Thankfully, the above problem is something that we can fix ourselves, since Swift’s new concurrency system ships with a continuation mechanism that lets us retrofit existing code with async/await support.

Here’s how we could use that mechanism to replicate the above async/await-powered URLSession API in order to make it available all the way back to iOS 13:

@available(iOS, deprecated: 15.0, message: "Use the built-in API instead")
extension URLSession {
    func data(from url: URL) async throws -> (Data, URLResponse) {
        try await withCheckedThrowingContinuation { continuation in
            let task = self.dataTask(with: url) { data, response, error in
                guard let data = data, let response = response else {
                    let error = error ?? URLError(.badServerResponse)
                    return continuation.resume(throwing: error)
                }

                continuation.resume(returning: (data, response))
            }

            task.resume()
        }
    }
}

Note how we add a custom deprecation annotation to the above extension, so that we’ll get a compiler warning whenever we’ll increase our app’s deployment target to iOS 15 or above.

With the above in place, the compiler error within our ModelLoader is now gone, as we’re now able to easily perform URLSession-based network calls using async/await, even within a backward-compatible code base. Really nice!

However, please note that the above code sample is just an example, as it doesn’t handle things like cancellation. For a more complete backward-compatible implementation of the above URLSession API, please check out AsyncCompatibilityKit over on GitHub.

It’s also important to point out that the above kind of work is not required for all async-marked system APIs. In fact, those that were automatically translated from completion handler-based Objective-C APIs are automatically made backward compatible — just like the concurrency features themselves, as well as standard library types like Task and AsyncStream.

To learn more about the above continuation system, and other techniques like it, check out “Connecting async/await to other Swift code”.