https://developer.apple.com/videos/play/wwdc2021/10132/

Create a generic networking layer using async/await.

James Rochabrun
Geek Culture
Published in
6 min readJun 11, 2021

--

Completion handlers are closures, self-contained blocks of functionality that can be passed around and used in your code. They get passed to a function as an argument and then called when that function is done.

They are used in iOS when we want to execute an asynchronous job, for example, let’s see how a generic API that uses a URLSessionDataTask to perform a request and decodes the response looks like. It will look something like this…

Generic networking layer — completion handlers

Here the public fetch function calls the private decoding task function internally. The decoding task function uses a URLSession data task to get resources from a server, as you can see the session data task is also a closure and inside of it we execute our escaping completion handlers once we get data, a response, or an error. There is nothing wrong with this, other than how long and unreadable something as simple can become.

There are some other options like Promises to help make this kind of code more compact and understandable but now in Swift 5.5 we can use the new async and await APIs to clean this up a bit and even make our code safer, so let’s see how we can refactor this generic code but now using async-await.

We still use the same APIError just for the purpose of the example, but now we use async/await, the fetch function still takes a decodable type and a request as arguments, but instead of passing a completion handler now the function is async.

  1. When you mark a function async, the keyword should go just before throws in the function signature, like on this example, or before the arrow if the function does not throw. Now if the response is successful and the object was decoded, the method will just return it.
  2. iOS 15 introduced a new URLSession method data(for:delegate:), this method returns non-optional data and a non-optional response. If you go into the GenericAPI example you will see that we use dataTask(with:completionHandler:) instead, this method returns optional data, an optional response, and an optional error. Both methods are asynchronous and do pretty much the same (well, not really, the “old” API returns also the error info but the async does not, I am not sure why Apple decided to remove this piece of info in the return value 🤷🏽‍♂️). This async method throws so the caller needs to add a try keyword, and because it is async it also needs the await keyword.

If an expression has multiple async function calls in it, you only need to write await once, just like you only need one try for an expression with multiple throwing function calls.

3. Inside a do/catch block we return the decoded value if the decoding operation was successful.

4. Else, we throw.

There is one thing that you need to know when calling an async method, let’s see it with an example, we will create a remote object that has a Published property that will be set with the response of our async fetch function…

final class FeedRemote: ObservableObject {
/// 1
private let client = Client()
/// 2
@Published private(set) var musicItems: [MusicItemViewModel] = []
///3
var
request: URLRequest = {
let urlString = "https://rss.itunes.apple.com/api/v1/us/apple-music/coming-soon/all/50/explicit.json"
let url = URL(string: urlString)!
return URLRequest(url: url)
}()
///4
func
fetchMusicItems() {
let container = try await client.fetch(type: Container<MusicItem>.self, with: request)
musicItems = container.feed.results.map { MusicItemViewModel(musicItem: $0) }
}
  1. Client is a class that conforms to the AsyncGenericAPI.
  2. The musicItems property will publish the changes from the response using the Combine framework.
  3. The request, in this case, will be using the RSS feed Apple generator API.
  4. The fetchMusicItems is called from a view controller or any instance that has an initialized FeedRemote object. Inside of this function, we call our async fetch function, it has the try and async keywords because as we know this is an async and throwing call.

If you try to run the app, the compiler will show you a couple of errors…

The first error you can see is Errors thrown from here are not handled”, this is an easy one, you just need to wrap your code in a do/catch block, the second one is more interesting “`async` call in a function that does not support concurrency” what that this means? Well, this just means that you need to “bridge” an asynchronous call from a synchronous context, no worries, this is easy to fix, you can just mark the `fetchMusicItems` as async like this…

func fetchMusicItems() async { <---- marking the function as asyncdo {let container = try await client.fetch(type:  Container<MusicItem>.self, with: request)
musicItems = container.feed.results.map { MusicItemViewModel(musicItem: $0) }
} catch {
print("The error is --- \((error as! APIError).customDescription)")
}
}

This solves the problem here, but it just moved the issue one-level app, you can continue marking the top-level function as async but there is a better way, you can use the async closure to execute your async call asynchronously, the final implementation looks like this…

func fetchMusicItems() {async { <--- async closure :) 
do {
let container = try await client.fetch(type: Container<MusicItem>.self, with: request)
musicItems = container.feed.results.map { MusicItemViewModel(musicItem: $0) }
} catch {
print("The error is --- \((error as! APIError).customDescription)")
}
}
}

That’s it, by using the async closure now you can execute your async functions without the need to mark a top-level function as async 🎉

There is one last thing that I will like to show you; if you compare the implementation of the fetch function in the GenericAPI vs the one in the AsyncGenericAPI you can see that in the first one we jump to the main thread using CGD when we get the result, so how do we ensure that our AsyncGenericAPI fetch method also gives us the results on the main thread for UI updates?

Well, we can also use GCD for this and push our code to the main thread like this…

func fetchMusicItems() {async {
do {
let container = try await client.fetch(type: Container<MusicItem>.self, with: request)
DispatchQueue.main.async { <--- GCD main queue
self.musicItems = container.feed.results.map { MusicItemViewModel(musicItem: $0) }
}
} catch {
print("The error is --- \((error as! APIError).customDescription)
}
}
}

This works but now there is a better way, you can use the @MainActor property wrapper to warranty that your code will run on the main thread without even manually specify it. Swift will automatically queue up any work to happen there as if we had used DispatchQueue.main ourselves.

You can use the @MainActor just in your functions or properties but for this example where the remote class will always publish results to display in a UI, it makes more sense to use it in the whole class instead. By doing so every function or property inside it will have the benefits that a @MainActor provides.

The code will look like this…

@MainActor
final class FeedRemote: ObservableObject {
/// FeedRemote implementation ....
}

This is just a small set of tools that Swift 5.5 brings with its new concurrency APIs, there are lots more and if you are interested here is a curated list of all the WWDC2021 talks about this topic. 😁

Meet async/await in Swift

Protect mutable state with Swift actors

Explore structured concurrency in Swift

Meet AsyncSequence

Swift concurrency: Behind the scenes

Use async/await with URLSession

Swift concurrency: Update a sample app

Swift concurrency: Behind the scenes

Discover concurrency in SwiftUI

Understand and eliminate hangs from your app

Here is a sample project that contains all the code and more for this post. 🤓

Here is a discussion around Swift concurrency support for earlier versions. 😭🙏

Thanks. 🤖

--

--