EXPEDIA GROUP TECHNOLOGY — ENGINEERING
Async/Await in Swift
Improving concurrent code
During WWDC 2021, Apple introduced a great improvement for asynchronous programming: async
and await
. Those two keywords will already be familiar to anyone who has written asynchronous code in JavaScript. Let’s dive into the following example to understand more about how to use them.
Problem area
Let’s develop a tour and travel application. One simple feature is that users can cancel and track refund status for their trip. We can call an API to get all the user’s trip lists followed by another API call to get refund status for each trip. Let’s write some code and see it in action!
Trip details model
In the following snippet, we create a model to store trip details. We can review status
to know whether a trip is cancelled
, confirmed
or booked
.
/// Model to store Trip Detailsstruct Trip: Codable { let id: String let itinerary: String let status: TripStatus enum TripStatus: String, Codable { case confirmed case cancelled case booked case unknown }
}
Let’s implement our desired feature using closures.
Trailing closures
func getAllCancelledTrips(for userId: String, completion: @escaping ([Trip]) -> Void) { APIManager.shared.getAllTrips(for: userId) { [weak self] trips in guard let self = self else { return } let cancelledTrips = trips.filter { $0.status == .cancelled } completion(cancelledTrips) }}
What is happening above?
- We call an API to get all the trips for the given user ID.
- Next, we filter out cancelled trips and returned the response using the completion block.
That code looks fine, but what if we need multiple nested closures?
Nested closures
In the following example, after fetching cancelled trips we call a separate API to get refund status of the trip. For this simple task, we have two nested closures. Multiple nested closures make the code look messy and hard to understand.
APIManager.shared.getAllTrips(for: userId) { [weak self] trips in guard let self = self else { return } let cancelledTrips = trips.filter { $0.status == .cancelled } for trip in cancelledTrips { APIManager.shared.getRefundStatus(for: trip.id) { isRefunded in /// Do necessary stuffs
} } completion(cancelledTrips)}
Calling async and await to the rescue
Let’s now see how async
/await
can simplify the previous code!
Calling function
We add async
right before the return type in the function definition. This indicates that something asynchronous happens within the function.
/// Calling functionfunc getAllTrips(for userId: String) async -> [Trip] { /// Method definition goes here}
Caller function
We have to use the await
keyword in front of the caller function. By doing this, the current execution will be paused until the result is obtained.
/// Caller functionfunc getAllCancelledTrips(for userId: String) async -> [Trip] { let trips = await APIManager.shared.getAllTrips(for: userId) let cancelledTrips = trips.filter { $0.status == .cancelled } return cancelledTrips}
Parallel execution
The previous example shows the use case of serial execution. If we want to execute this call asynchronously we can simply write async
before the variable name. Consequently, we need to add the await
keyword whenever we refer to that variable, like where we filter the full list of trips.
func getAllCancelledTrips(for userId: String) async -> [Trip] { /// Added async keyword to allow asynchronous call of the
/// following method async let trips = await APIManager.shared.getAllTrips(for: userId) let cancelledTrips =
await trips.filter { $0.status == .cancelled } return cancelledTrips}func getRefundStatusFor(userId: String) async { async let cancelledTrips = await getAllCancelledTrips(for: userId) for trip in await cancelledTrips { APIManager.shared.getRefundStatus(for: trip.id) { isRefunded in // Do necessary stuff }
}
}
To summarize the advantages of using async
/await
:
- Less code
- Easier to read
- Simple