
Migrating your project to the new Swift concurrency using async/await

Bianca
iOS Developer
10 min
Jul 10, 2023
Async/await starting with Swift 5.5
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.
What does it mean for the iOS app development process?
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.
Example project: News app
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.
Components
Let’s start by examining the networking components and the view model:
1. APIService
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.
2. APIProtocol
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.
3. NewsAPI
This enum contains all the news-related API endpoints. For simplicity, we’ll focus on the ‘getNews’ case for now.
4. NewsViewModel
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).
Migrating to async/await
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.
Grouped API calls
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?
Migrating a networking layer that uses Alamofire
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:).
Wrapping up
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’.
pack knowledge

Is your digital product ready for the European Accessibility Act (EAA)? Take the assessment to find out!

Oana
Marketing Specialist
5 min
Mar 20, 2025
With the 2025 deadline approaching, ensuring your website or app meets accessibility standards is important. Our free EAA assessment helps you quickly check compliance and identify areas for improvement. At Wolfpack Digital, we specialize in making digital products accessible, user-friendly, and future-proof. Take the assessment today and ensure your platform is inclusive for all users!

How Much Does Web Development Cost? A Complete Guide

Oana
Marketing Specialist
10 min
Feb 26, 2025
Building a website is more than just designing pagesβitβs about creating a functional, high-performing, and scalable digital presence that meets your business goals. Whether you're launching a simple business website, an e-commerce store, or a custom web platform, the cost and approach will vary based on complexity, functionality, and long-term needs.

UX/UI Design: Collaborating with a Software Development Agency When You're New to the Game

Cristian
Head of UX/UI Design
7 min
Feb 18, 2025
This guide will walk you through every stage of the UX/UI design process, from the initial conversation to launching a product you can be proud of. Understanding the design thinking process, which includes empathizing, defining, ideating, prototyping, and testing, is crucial for creating user-friendly products and solutions.
Brief us and letβs work together