Swift Async and Await

Bassem Qoulta
6 min readSep 16, 2023

--

Photo by Steve Johnson on Unsplash

Asynchronous code

Asynchronous code can be suspended and resumed later, although only one piece of the program executes at a time. Suspending and resuming code in your program lets it continue to make progress on short-term operations like updating its UI while continuing to work on long-running operations like fetching data over the network or parsing files.

Parallel code

Parallel code means multiple pieces of code run simultaneously — for example, a computer with a four-core processor can run four pieces of code at the same time, with each core carrying out one of the tasks.

What is Async and Await?

An asynchronous function in Swift can give up the thread that it’s running on, which lets another asynchronous function run on that thread while the first function is blocked. When an asynchronous function resumes, Swift doesn’t make any guarantee about which thread that function will run on.

To indicate that a function is asynchronous, you write the async keyword in its declaration after its parameters

When calling an asynchronous method, execution suspends until that method returns, so you have to write await in front of the call to mark the possible suspension point. Inside an asynchronous method, the flow of execution is suspended only when you call another asynchronous method which means every possible suspension point is marked with await.

So Imagine that we need to download a photo, so we will create a function which does the network request and when the data comes back we call the completion closure to notify the caller like this:

func downloadImage(from url: URL, completion: @escaping (UIImage?) -> ()) {
URLSession.shared.dataTask(with: URLRequest(url: url)) { data, response, error in
if let data = data,
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode >= 200 && httpResponse.statusCode <= 300 {
completion(UIImage(data: data))
} else if error != nil {
completion(nil)
}
}.resume()
}

So SE-0296 gave us a super sweet away to handle that by writing async to the function and use await to wait to the request to finish and download the photo data.

func downloadImage(from url: URL) async -> UIImage? {
do {
let (data, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode >= 200 && httpResponse.statusCode <= 300 {
return UIImage(data: data)
}
return nil
} catch {
return nil
}
}

You can see that we just got rid of the completion closure from the first function and added async/await to the second function.

How to use Async/Await to avoid Pyramid of doom

Imagine that we need to execute a sequence of simple asynchronous operations which always requires deeply-nested closures, like imagine that you need to download an image, then resize it, decode it and finally upload it and notify when its done

func processImage(from url: URL, completion: @escaping (Bool) -> ()) {
downloadImage(from: url) { image in
resize(image: image) { resizedImage in
decode(image: image) { decodedImage in
upload(image: decodedImage) { response
completion(response.sccuess)
}
}
}
}
}

So after SE-0296 gave us a super sweet away to handle that we can write the code super clean and neat like this:

func processImage(from url: URL) async -> Bool{
let image = await downloadImage(from: url)
let resizedImage = await resize(image: image)
let decodedImage = await decode(image: resizedImage)
let response = await upload(image: decodedImage)
return response.isSccuess
}
  • We started by downloading the image and because downloadImage function is async we have to use await which suspends execution while it waits for that function to return.
  • While this code’s execution is suspended, some other concurrent code in the same program runs. For example, maybe a long-running background task continues updating a list of new photo galleries. That code also runs until the next suspension point, marked by await, or until it completes.
  • After downloadImage returns, the code will continue execution starting at that point. It assigns the value that was returned to image.
  • The next await marks the call to the resize(image:) function and again the code pauses execution until that function returns, giving other concurrent code an opportunity to run.
  • After resize(image:) returns, the code will continue execution starting at that point. It assigns the value that was returned to resizedImage.
  • and so on until the all the async functions finished their work and finally return the state of the response.

The possible suspension points in your code marked with await indicate that the current piece of code might pause execution while waiting for the asynchronous function or method to return. Swift suspends the execution of your code on the current thread and runs some other code on that thread instead. Because code with await needs to be able to suspend execution, only certain places in your program can call asynchronous functions or methods:

  • Code in the body of an asynchronous function, method, or property.
  • Code in the static main() method of a structure, class, or enumeration that’s marked with @main.
  • Code in an unstructured child task.

Calling Asynchronous Functions in Parallel

Calling an asynchronous function with await runs only one piece of code at a time. While the asynchronous code is running, the caller waits for that code to finish before moving on to run the next line of code.

As in the previous example each function had to wait to the previous function to return its result to continue executing the next function, so what about if you need to run more than one async function in the same time?

To call an asynchronous function and let it run in parallel with code around it, write async in front of let when you define a constant, and then write await each time you use the constant, so imagine that you need to download 4 photos

async let image1 = downloadImage(from: url1)
async let image2 = downloadImage(from: url2)
async let image3 = downloadImage(from: url3)
async let image4 = downloadImage(from: url4)

let images = await [image1, image2, image3, image4]

All 4 calls to downloadImage will start without waiting for the previous one to complete, and none of these function calls are marked with await because the code doesn’t suspend to wait for the function’s result. Instead, execution continues until the line where images is defined — at that point, the program needs the results from these asynchronous calls, so you write await to pause execution until all 4 images finish downloading.

Task Groups

Lets say that we have an array has bunch of urls and we need to download the image for each url, but we need to execute all the network requests in the same time parallel.

SE-0304 gave us Task Group which defines a scope in which one can create new child tasks programmatically. As the child tasks within the task group scope must complete when the scope exits, and will be implicitly cancelled first if the scope exits with a thrown error.

The withTaskGroup API gives us access to a task group, and controls the lifetime of the child tasks we subsequently add to the group using its addTask() method. By the time withTaskGroup finishes executing, we know that all of the subtasks have completed.

func download() async -> [UIImage] {
let urls = [
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
]

return await withTaskGroup(of: UIImage.self) { group in
for url in urls {
group.addTask {
await self.downloadImage(url: url)!
}
}
defer {
group.cancelAll()
}
return await group.reduce(into: [UIImage]()) {
$0.append($1)
}
}
}

We created withTaskGroup and inside we create a new task for each url to download the image for each url and after than we await for the result of the group to return all the downloaded images in the same time.

--

--

Bassem Qoulta

Learning is always fun, so I am learning new stuff and share it here! Berlin, Germany