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 onDispatchGroup
. ð to Threading Deadlocks when synchronisingDispatchGroup
s across differentDispatchQueue
s.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 addif #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
}
}