How to make async / await in Swift?

We have to await!

The async-await Swift Evolution proposal SE-0296 async/await was accepted after 2 pitches and revision modifications recently on December 24th 2020. This means that we will be able to use the feature in Swift 5.5. The reason for the delay is due to backwards-compatibility issues with Objective-C, see SE-0297 Concurrency Interoperability with Objective-C. There are many side-effects and dependencies of introducing such a major language feature, so we can only use the experimental toolchain for now. Because SE-0296 had 2 revisions, SE-0297 actually got accepted before SE-0296.

General Use

We can define an asynchronous function with the following syntax:

private func raiseHand() async -> Bool {
  sleep(3)
  return true
}

The idea here is to include the async keyword alongside the return type since the call site will return (BOOL here) when complete if we use the new await keyword.

To wait for the function to complete, we can use await:

let result = await raiseHand()

Synchronous/Asynchronous

Defining synchronous functions as asynchronous is ONLY forward-compatible - we cannot declare asynchronous functions as synchronous. These rules apply for function variable semantics, and also for closures when passed as parameters or as properties themselves.

var syncNonThrowing: () -> Void
var asyncNonThrowing: () async -> Void
...
asyncNonThrowing = syncNonThrowing // This is OK.

Throwing functions

The same consistency constraints are applied to throwing functions with throws in their method signature, and we can use @autoclosures as long as the function itself is async.

We can also use try variants such as try? or try! whenever we await a throwing async function, as standard Swift syntax.

rethrows unfortunately still needs to go through Proposal Review before it can be incorporated because of radical ABI differences between the async method implementation and the thinner rethrows ABI (Apple wants to delay the integration until the inefficiencies get ironed out with a separate proposal).

Networking callbacks

This is the classic use-case for async/await and is also where you would need to modify your code:

// This is an asynchronous request I want to wait
await _ = directions.calculate(options) { (waypoints, routes, error) in

Change to this:

func calculate(options: [String: Any]) async throws -> ([Waypoint], Route) {
    let (data, response) = try await session.data(from: newURL)
    // Parse waypoints, and route from data and response.
    // If we get an error, we throw.
    return (waypoints, route)
}
....
let (waypoints, routes) = try await directions.calculate(options)
// You can now essentially move the completion handler logic out of the closure and into the same scope as `.calculate(:)`

The asynchronous networking methods such as NSURLSession.dataTask now has asynchronous alternatives for async/await. However, rather than passing an error in the completion block, the async function will throw an error. Thus, we have to use try await to enable throwing behaviour. These changes are made possible because of SE-0297 since NSURLSession belongs to Foundation which is still largely Objective-C.

Code impacts

  • This feature really cleans up a codebase, goodbye Pyramid of Doom 👋!

  • As well as cleaning up the codebase, we improve error handling for nested networking callbacks since the error and result are separated.

  • We can use multiple await statements in succession to reduce the dependency on DispatchGroup. 👋 to Threading Deadlocks when synchronising DispatchGroups across different DispatchQueues.

  • Less error-prone because the API is clearer to read. Not considering all exit paths from a completions handler, and conditional branching means subtle bugs can build up that are not caught at compile time.

  • async / await is not back-deployable to devices running < iOS 13, so we have to add if #available(iOS 13, *) checks where supporting old devices. We still need to use GCD for older OS versions.


(Note: Swift 5 may support await as you’d expect it in ES6!)

What you want to look into is Swift's concept of "closures". These were previously known as "blocks" in Objective-C, or completion handlers.

Where the similarity in JavaScript and Swift come into play, is that both allow you to pass a "callback" function to another function, and have it execute when the long-running operation is complete. For example, this in Swift:

func longRunningOp(searchString: String, completion: (result: String) -> Void) {
    // call the completion handler/callback function
    completion(searchOp.result)
}
longRunningOp(searchString) {(result: String) in
    // do something with result
}        

would look like this in JavaScript:

var longRunningOp = function (searchString, callback) {
    // call the callback
    callback(err, result)
}
longRunningOp(searchString, function(err, result) {
    // Do something with the result
})

There's also a few libraries out there, notably a new one by Google that translates closures into promises: https://github.com/google/promises. These might give you a little closer parity with await and async.


Thanks to vadian's comment, I found what I expected, and it's pretty easy. I use DispatchGroup(), group.enter(), group.leave() and group.notify(queue: .main){}.

func myFunction() {
    let array = [Object]()
    let group = DispatchGroup() // initialize

    array.forEach { obj in

        // Here is an example of an asynchronous request which use a callback
        group.enter() // wait
        LogoRequest.init().downloadImage(url: obj.url) { (data) in
            if (data) {
                group.leave() // continue the loop
            }
        }
    }

    group.notify(queue: .main) {
        // do something here when loop finished
    }
}