Async await

So Async and Await has finally come to Swift, with the release of Xcode 13! Finally! Here’s some examples for practically using it with an existing codebase, interfacing with normal (synchronous) functions.

How can I wrap a callback function with an async one?

Say you have a function with a completion Result<T, Error> callback, you can wrap it to convert it to async/await code using withCheckedThrowingContinuation like so:

// Convert a callback-style function to async await.
func getUser(name: String) async throws -> String {
    try await withCheckedThrowingContinuation {
        continuation in
        getUserViaCallback(name: name, completion: {
            result in
            continuation.resume(with: result)
        })
    }
}

A Result.failure will thus become a thrown error, and a Result.success will be a returned String.

For reference, the function with the completion handler that we’re wrapping above is:

// Completion callback (non async/await) network function.
func getUserViaCallback(name: String, completion:
    @escaping (Result<String, Error>) -> ()) {
        
    enum MyErrors: Error {
        case encoding
        case badUrl
        case noData
        case badData
    }

    guard let safeName = name.addingPercentEncoding(
        withAllowedCharacters: .urlQueryAllowed) else {
        completion(.failure(MyErrors.encoding))
        return
    }
    let urlStr = "https://hacker-news.firebaseio.com/v0/user/\(safeName).json"
    guard let url = URL(string: urlStr) else {
        completion(.failure(MyErrors.badUrl))
        return
    }
    URLSession.shared.dataTask(with: url, completionHandler: {
        data, response, error in
        if let error = error {
            completion(.failure(error))
        }
        guard let data = data else {
            completion(.failure(MyErrors.noData))
            return
        }
        guard let string = String(data: data, encoding: .utf8) else {
            completion(.failure(MyErrors.badData))
            return
        }
        let name = String(string.prefix(20)) // Silly example :)
        completion(.success(name))
    }).resume()
}

How can I orchestrate multiple async calls?

This is where async/await shines: Orchestrating multiple calls. This is a life-saver if you want to make readable code when you have a tricky backend that requires multiple calls to perform one ‘thing’:

// Example of how you can use await to elegantly orchestrate multiple network calls.
func getUsers() async throws -> [String] {
    let a = try await getUser(name: "patio11")
    let b = try await getUser(name: "dang")
    let c = try await getUser(name: "pg")
    let d = try await getUser(name: "llambda")
    return [a, b, c, d]
}

How can I call an async function from a normal (synchronous) one?

Async/await is viral: Any async function must be called by another async one, all the way up the chain. Which obviously isn’t going to be possible with a UIKit app. The ‘trapdoor’ to exit this requirement is the async function. The example below shows how you can use this to convert an async function into one with a familiar Result completion callback. It would be the responsibility of the caller to dispatch to the Main queue to update the UI:

// Call an async function from normal sync code.
// The completion is on an undefined thread/queue.
func getUsersWithCallback(completion: @escaping (Result<[String], Error>) -> ()) {
    async {
        do {
            let users = try await getUsers()
            completion(.success(users))
        } catch {
            completion(.failure(error))
        }
    }
}

What thread does it run on?

Short answer: a GCD Global queue.

Long answer: Apple’s documentation is pretty vague when it comes to which thread your async functions run on. If you’re creating a server app, maybe this isn’t an issue. But if you’re writing a UIKit app, this is very important: You need to be on the main thread to do any UI updates, of course!

When calling an async func from a normal synchronous one, you use this helper:

async(priority: Task.Priority? = nil, operation: () async -> T)

The priorities are basically the same as the GCD ones you’d specify when asking for a global queue:

DispatchQueue.global(qos: DispatchQoS.QoSClass)

Thus I assume async functions simply run on the GCD global queue of whatever priority/QOS you specify (or you can leave it nil to get the default background queue).

What version of iOS does it need?

As of writing (July 2021), your app will require iOS 15 to use async+await. Bummer. However: this is listed as an ‘issue’ in the Xcode beta release notes, which people are taking as a hint that Apple is hard at work resolving this so that your app will not require iOS 15 to use it, thus making it an option for a shipping app. Fingers crossed that once Xcode 13 comes out of beta, async/await will have some form of backwards compatibility.

Thanks for reading, I hope this is helpful, God bless :)

Photo by freezydreamin on Unsplash

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.

Chris Hulbert

(Comp Sci, Hons - UTS)

iOS Developer (Freelancer / Contractor) in Australia.

I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!

Get in touch:
[email protected]
github.com/chrishulbert
linkedin



 Subscribe via RSS