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 DispatchGroup
s across different DispatchQueue
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 add if #available(iOS 13, *)
checks where supporting old devices. We still need to use GCD for older OS versions.