Async/await is part of the new structured concurrency changes that arrived in Swift 5.5. Concurrency in Swift means allowing multiple pieces of code to run at the same time. With the new async functions and await statements, we can define asynchronous sequences.
As an iOS developer, you might ask yourself, “Why should you adopt it and update your current project to this structure”. While adopting this feature only when starting a new project can be an easier task, refactoring your current one would achieve a much cleaner and better-structured code.
Here are some benefits I’ve found after migrating to async/await:
1. Eliminates repetitive and long completion handlers. @escaping (Result<T, APIError>) -> Void can become frustrating to write repeatedly. With async/await, you can avoid nested completion handlers and make your code more readable and maintainable.
2. Improved decision and error handling: async/await pairs beautifully with do try catch blocks, resulting in well-structured and clear error handling in your code.
3. Reduced risk of hanging completions: with async/await, the risk of forgetting to call completion(...) is greatly reduced. You must either return a value or throw an error, eliminating ambiguous scenarios.
4. Synchronous-looking asynchronous code: async/await allows you to write asynchronous code that resembles synchronous code, making it easier to understand and reason about.
5. Less code to write: async/await simplifies your codebase, resulting in shorter and more readable files.
6. Considering all these benefits, let's find out how to migrate the code in some of the most common iOS Development scenarios. We'll start with a traditional async programming project, going through multiple layers of a well-structured iOS application.
7. Then, we'll see how to handle multiple API calls and how to group them. Finally, since many projects out there are using Alamofire on the networking side, we'll cover a migration example for those as well.
For this example, we’ll work on a News app that fetches and displays news articles from a REST API. The project structure includes a view, a view model, a data model, and a simple networking layer.
Let’s start by examining the networking components and the view model:
This struct has a static func request<T: Decodable>(route: APIProtocol) async throws -> T. The method expects a route conforming to the ‘APIProtocol’ and a completion handler. It performs a basic URL request using a data task. The retrieved data is then decoded, and the appropriate data or error is passed to the completion handler.
Acting as an interface for API routes, this protocol defines a method, path, optional body parameters, and query items. The ‘asURLRequest()’ method builds a basic URLRequest.
This enum contains all the news-related API endpoints. For simplicity, we’ll focus on the ‘getNews’ case for now.
The view model only has a news items array and fetches the data using the APIService’s request method (in a more complex project, we would have another layer - repository/data source, to handle this).
1. Updating the APIService
Let’s go through the changes we’ve made to this method:
1. The signature is now marked with ‘async’ and ‘throws’ and returns a ‘Decodable’ type.
2. Since the method is marked as ‘async’, we can directly use URLSession’s ‘async data(for: )’. This method returns a tuple containing the ‘Data’ object and the ‘URLResponse’.
3. The decoded data can be directly returned instead of using a completion handler.
4 & 5. Error handling is simplified using ‘throw’, ensuring that you either return a value or throw an error. This prevents the omission of completion handler calls in certain scenarios.
By refactoring this file, we’ve reduced the number of lines from 38 to 33 (which means around 13% of the code was removed). You might think it’s not a big difference, but keep in mind that this is a very simple example. Usually, our projects hold much more complex code.
2. Updating the view model
Now that we’ve refactored the APIService, let’s take a look at how the view model will look after updating it as well:
So, what changed?
1. We’re now creating a ‘Task’, which is a unit of asynchronous work. Without creating the task, we’d have the following error:
This error states that we either mark the getNews method as async as well or create a Task. Note that if I were to mark this method as async, the same error would occur when calling it in the init.
2. We have the do try catch structure. I know that not everyone is a fan of it, but it does add clarity (it’s also a popular structure in many other programming languages).
3. We are calling the APIService’s request method with try await. These try await keywords underline the fact that the execution of the following code will continue only after a response is received.
4. Because the awaited request call happens on a background thread, we need to ensure we’re handling the UI on the main thread. So, before setting the published news array, we’re using this await MainActor.run to wait for the main thread to become available and switch to it for updating the interface.
Ok, but what happens if you need to make multiple API calls?
To demonstrate how the view model would look if we want to await the completion of two or more asynchronous functions, let’s add an API call that would fetch another section called People of the Day.
The view model would look like this:
But, because the second API call we’ll be made right after the first one, if the first one fails, the error will be caught, and no data will be displayed.
To avoid this, we could have two separate methods, a loadNews() and a loadPeopleOfTheDay(). This way, if one of them fails and the other doesn’t, the received data will be displayed and the error handled.
Async/await also provides a way to group tasks and await for all of them to complete before moving on to the next execution.
To demonstrate this, I’ll add another API call, ‘getReadTime’, which will fetch the duration of reading a specific article in seconds. We’ll assume that, for some reason, we have to make this separate API call to get this information for each news article.
To achieve having grouped tasks using async/await, we can use await withThrowingTaskGroup(of:).
This method is a bit more complicated, so let’s take a look at what the documentation says about it:
And looking at our code:
1. A group returns a result. In our example, the child tasks don’t return a certain value; they just update the NewsItems accordingly. In our case, the result is (), hence the _ = try await withThrowingTaskGroup(of: … ).
2. For each news item, we’re creating a ‘loadReadTime’ task and adding it to the group. 3.
3. try await group.waitForAll() is pretty self-explanatory; we want to wait for all the tasks to finish before returning.
Alright, that was fairly easy. But what if you’re using a third-party networking SDK that doesn’t have support yet for async/await?
Updating to async/await if you’re using Alamofire is not that complicated either:
1. As Alamofire doesn’t have stable support yet for async/await (the SDK doesn’t have async methods yet), we can use await withCheckedThrowingContinuation. This is such an excellent tool; it allows you to bridge between Swift’s async/await concurrency model and older asynchronous APIs that use completion handlers. It enables you to work with existing asynchronous code that hasn’t been updated to support async/await.
2. If the Alamofire result is ‘success’, we call continuation.resume(returning:).
3. If the Alamofire result is ‘failure’, we call continuation.resume(throwing:).
Code cleanliness and readability are as important as maintaining our mobile applications up to date, using the latest technologies and tools. Also, making this adoption pairs nicely with refactoring existing code and bringing some freshness to older iOS apps.
And to finish with a just slightly altered English proverb:
‘Good things come to those who await’.
Stay up to date with the tech solutions we build for startups, scale-ups and companies around the world. Read tech trends and news about what we do besides building apps.