Why use swift async-await?

Swift New Asynchronous Programming

Ashok Rawat
6 min readOct 19, 2022
source

Swift development involves a lot of asynchronous programming using closures and completion handlers, but these APIs are hard to use. From Swift 5.5, Xcode 13 introduced built-in support for writing asynchronous and parallel code in a structured way using async/await.

async-await

Using async-await we can write asynchronous code without using completion handlers to return values. Structured concurrency improves the readability of complex asynchronous code in Swift.

  • async: Indicates that a function/property is asynchronous. It further lets you suspend the execution of the code until an asynchronous function/property returns a value or result.
  • await: Indicates that your code might halt its execution while it awaits the return of an asynchronous function/property.

Below are some steps we use async/await to make the function/property asynchronous.

  1. Mark function/property using the async keyword at the end of a function/property name in the definition.
  2. If the async function/property is going to raise an error, mark it with the throw keyword which follows the async keyword.
  3. Function/property has returned the success value. If the callee throws an error, it will be handled in the do-catch block on the caller side.
  4. We cannot call directly async function/property from synchronous code, It needs to wrap into Task where it will execute parallelly on the background thread.

Completion Handler is unstructured whereas async-await follows the structural sequential pattern. Below code snippets shows unstructured, structural pattern of closures and async/await respectiviely.

The async-await function allows for structured concurrency and hence improves the readability of complex asynchronous code in Swift. You can see the below code using async-await.

You can see below the calling method returns before the images are fetched using the completion handler. When the result is received, we go back into the completion callback. It is unstructured order of execution and hard to follow. If we performed another asynchronous method within our completion callback it would add another closure callback. Each closure adds another level of indentation, making it harder to follow the execution order.

What are Tasks?

All asynchronous functions run as part of some task. When a function makes an async call, the called function is still running as part of the same task (and the caller waits for it to return). Similarly, when a function returns from an async call, the caller resumes running on the same task. Async code cannot be directly called from the synchronous method. In order to invoke async code from the synchronous method, we need to create a task and call the async function from it. It creates a bridge between the sync/async code. Task creates an environment where you can execute and manage asynchronous work on a separate thread. We can run, pause, and cancel asynchronous code through the Task type.

Task {
let resultImage = try await fetchThumbnail(for: imageURL)
... // futher work
... // take over (asynchronously)
}

While creating a task, it’s important to specify the priority too based on how urgent the task is. Although you can directly assign a priority to a task when it’s created, if you don’t then Swift will follow three rules for deciding the priority automatically:

  1. If the task was created from another task, the child task will inherit the priority of the parent task.
  2. If the new task was created directly from the main thread as opposed to a task, it’s automatically assigned the highest priority of userInitiated.
  3. If the new task wasn’t made by another task or the main thread, Swift will try to query the priority of the thread.

The lowest priority is the background where it can perform the operation not so urgent from the user's perspective. The highest priority is high which is synonymous with userInitiated which indicates the task is important to the user. The default priority is medium where it will be treated the same way as other operations.

Running multiple async functions in parallel

If we want to run multiple unrelated async functions in parallel, we can wrap them up in their own tasks which will run in parallel. The order in which they execute is undefined.

Task(priority: .medium) {
let result1: UIImage = try await fetchThumbnail(for: profileURL)
}

Task(priority: .medium) {
let result2: UIImage = try await fetchThumbnail(for: profileThumbnailURL)
}

Task(priority: .medium) {
let result3: UIImage = try await fetchThumbnail(for: productImageURL)
}

Multiple async operations in parallel and getting results at once (Concurrent Binding)

We can run multiple async operations in parallel and get results at once. It is also called async let concurrent binding.

Task(priority: .medium) {
do {

async let image1 = try await fetchThumbnail(for: productImageURL1)

async let image2 = try await fetchThumbnail(for: productImageURL2)

async let image3 = try await fetchThumbnail(for: productImageURL3)

let imagesArray = try await [image1, image2, image3]

} catch {
// Handle Error
}
}

Asynchronous Sequences

Swift 5.5’s new concurrency system introduces the concept of asynchronous sequences and iterators. An AsyncSequence resembles the Sequence type — offering a list of values you can step through one at a time — and adds asynchronicity. An may have all, some, or none of its values available when you first use it. Instead, you use await to receive values as they become available.

for await i in Counter(howHigh: 10) {   
print(i, terminator: " ")
}

AsyncSequence is an asynchronous variant of the Sequence and AsyncSequence is just a protocol. AsyncSequence protocol provides an AsyncIterator and takes care of developing and potentially storing values. In creating Asynchronous iterations, we have to conform the AsyncSequence protocol and implement the makeAsyncIterator method.

Async properties

We can make the property async for that we need to add async after getter of the property.

extension UIImage { 
// async property
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 300, height: 300)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}

Only read-only properties can be async we need to create an explicitly setter for that property. If we try to provide a setter for the async property, the compiler will raise an error.

Async properties with throws

Async properties also support the throws keyword. We need to add the throws keyword after the async keyword in the property definition and use try await with the method that is responsible for throwing an error.

extension UIImage { 
var posterImage: UIImage {
get async throws {
do {
let ( data, _) = try await URLSession.shared.data(from: imageUrl)
return UIImage(data: data)!
} catch {
throw error
}
}
}
}

Using defer inside the async context

The defer block gets executed the last before exiting the context and guarantees to be executed making sure resource cleanup is not overlooked.

private func getMovies() {
defer {
print("Defer statement outside async")
}
Task {
defer {
print("Defer statement inside async")
}
let result = try? await ARMoviesViewModel().callMoviesAPIAsyncAwait(ARMovieResponse.self)
switch result {
case .failure(let error): print(error.localizedDescription)
case .success(let items): movies = items.results
case .none: print("None")
}
print("Inside the Task")
}
print("After the Task")
}
// After the Task
// Defer statement outside async
// Inside the Task
// Defer statement inside async

Continuation async APIs

Older code uses completion handlers for notifying us when some work has been completed when we are going to have to use it from an async function — either for a third-party library or our own functions but updating it to async would take a lot of work. With the help of continuation, we can wrap completion handlers into async APIs.

There are several kinds of continuations available:

  • withCheckedThrowingContinuation
  • withCheckedContinuation
  • withUnsafeThrowingContinuation
  • withUnsafeContinuation

Continuation must be resumed exactly once. Not zero times, and not twice or more times — exactly once.

Advantages of async-await

  • Avoid the Pyramid of Doom problem with nested closures
  • Reduction of code
  • Easier to read
  • Safety with async/await, a result is guaranteed, while completion blocks might or might not be called.

You can check out the demo project on Github.

In this article, we acknowledged the role of async-await within the new Swift Concurrency Model, and why it can be relevant for us. I hope this article has given you a few new insights into different aspects of async-await to use in real-life problems. You can also read my blog about Swift Actor to prevent Data Race.

Thanks for reading. If you have any comments, questions, or recommendations feel free to post them in the comment section below! 👇 and please share and give claps 👏👏 if you liked this post.

--

--

Ashok Rawat

Mobile Engineer iOS | Swift | Objective-C | Python | React | Unity3D