Async Let
This post is brought to you by Emerge Tools, the best way to build on mobile.
Look, I’m late to the boat on Swift Concurrency. It’s just one of those things I missed out of the gate. As I drift through the quotidian information stream my Twitter feed provides, I can’t help but notice that developers seem to really be enjoying the concurrency features. So I began to wonder, what am I really missing beyond the basics?
At work, I’ve started to take on rewriting a large part of our networking stack, dragging it from the Objective-C, block-based callbacks of yore and into the modern realm of await
, async
and all of his friends. Even though I could wrap up things in a Task
, tack on an async
adornment to my functions and move along, I’ve recently endeavored to actually understand what on earth is happening once I hit errors like these:
Nonisolated actor is doing things that are isolated to a MainActor.
Unchecked Sendable!
Yeah Concurrency!
Reference to captured var 'Something' in concurrently-executing things!
New Nouns and Verbs!
And so I did what any sane iOS developer would do, went over to hackingwithswift.com and found a guide.
Previously, I was already aware of most of the semantics around concurrency - for example, making something like this:
struct Weather: Codable {
var temp: Int
var uxIndex: Int
}
struct WeatherFetch {
func getTemp() async throws -> Int {
let decoder = JSONDecoder()
let (tempData, _) = try await URLSession.shared.data(from: URL(string: "https://foo.com/weather/temp")!)
let forecast = try decoder.decode(Weather.self, from: tempData)
return forecast.temp
}
}
let concatenatedThoughts = """
Also, these endpoint examples are not really gonna make any sense. Just go with it and realize that later in the post, I'll have a situation where I need to hit two different ones.
"""
All well and good, and to wit, this is already a massive improvement over what our Objective-C code looks like. But, there are often times where I need two bits of information from two (or more) different endpoints - a job us Objective-C dinosaurs would typically cosign to dispatch groups:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[self fetchTemp:^(NSUInteger temp) {
// Assign temp somewhere
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[self fetchUVIndex:^(NSUInteger deathRays) {
// Assign deathRays somewhere
dispatch_group_leave(group);
}];
// All fetch operations completed
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// All done
});
});
At first, I wrote a similar solution like this using Swift Concurrency:
func getForecast() async throws -> Weather {
let decoder = JSONDecoder()
let (tempData, _) = try await URLSession.shared.data(from: URL(string: "https://foo.com/weather/temp")!)
let tempForecast = try decoder.decode(Weather.self, from: tempData)
let (uvData, _) = try await URLSession.shared.data(from: URL(string: "https://foo.com/weather/uvIndex")!)
let uvForecast = try decoder.decode(Weather.self, from: uvData)
return Weather(temp: tempForecast.temp, uvIndex: uvForecast.uvIndex)
}
And that works just fine. The flow, however, goes like this:
- Get the temperature.
- Then, get the UV index.
And neither one requires the other to finish, it’s just that both need to finish. Enter async let
:
func getForecast() async throws -> Weather {
let decoder = JSONDecoder()
async let (tempData, _) = URLSession.shared.data(from: URL(string: "https://foo.com/weather/temp")!)
async let (uvData, _) = URLSession.shared.data(from: URL(string: "https://foo.com/weather/uvIndex")!)
let tempForecast = try await decoder.decode(Weather.self, from: tempData)
let uvForecast = try await decoder.decode(Weather.self, from: uvData)
return Weather(temp: tempForecast.temp, uvIndex: uvForecast.uvIndex)
}
Now, fetching the temperature and the UV index can both kick off immediately. No need to wait for one to finish, then begin the next request.
Neato. So, if you’ve got multiple network calls that don’t depend on one another - consider reaching for async let foo = call()
in lieu of let foo = await call()
.
Until next time ✌️