technical articlesOctober 4, 2021

How to use async/await in Swift 5.5

How to get started with async/await

Article presentation
Learn how to use the new async/await features in Swift 5.5.

What is async/await?

The release of Swift 5.5 has brought many new features and async/await features are one of the biggest ones. Although it was possible to write asynchronous code before or parallel code, it was harder to read and reason about. 

Now, thanks to async/await, you can write asynchronous code using a single line of code. This gives a great improvement to our code making it better semantically structured and, also, it addresses a lot of the problems of dealing with closures and delegates.

Here is an example of a data task that loads a URL and calls back a completion handler when done.

 1 func startLoading(url: URL, completionHandler: @escaping (String?, Error?) -> Void) { 
 2 	// Create a data task
 3 	let task = URLSession.shared.dataTask(with: url) { data, response, error in
 4 		if let error = error {
 5 			completionHandler(nil, error)
 6 			return 
 7 		}
 8 		// Make sure the HTTPURLResponse is correct
 9 		guard let httpResponse = response as? HTTPURLResponse,
10 		   (200...299).contains(httpResponse.statusCode) else {
11 		   completionHandler(nil, error)
12 		   return
13 		 }
14 		 
15 		 // Parse the response string
16 		 if let data = data,
17 			let string = String(data: data, encoding: .utf8) {
18 			completionHandler(string, nil)
19 		}
20 	}
21 
22 	// Resume the task
23 	task.resume()
24 }

This is method is hard to follow and it can be prone to mistakes such as forgetting to call the completion handler and forgetting to call return after an early completion handler callback. 

What we want to do actually is to have 2 operations in a sequence: fetch the data and transform it into a string. Here is how this will look using async/await:

 1 func startLoading(url: URL) async throws -> String { 
 2 	// Await the data and HTTPURLResponse
 3 	let (data, response) = try await URLSession.shared.data(for: url)
 4 	
 5 	// Make sure it is the right status code
 6 	guard let httpResponse = response as? HTTPURLResponse,
 7 		(200...299).contains(httpResponse.statusCode) else {
 8 			throw RequestError.badStatusCode
 9 	}
10 	
11 	// Parse the response string
12 	guard let string = String(data: data, encoding: .utf8) else {
13 		throw BadResponse.invalidFormat
14 	}
15 	
16 	return string
17 }

The first thing we can notice is that we no longer have to specify a completion handler in our method signature. Instead, we specify it returns a String that it is an async function and that it might throw an error. This is way simpler to reason about and allows us to throw errors when something goes wrong.

The async version of the data method is also simpler, it binds the data and the response to tuple if it succeeds, otherwise, it just throws an error that the function throws to the caller of the function. 

As you can see, throwing errors instead of passing back errors in callbacks is so way simpler, easier to understand, and implement.

In addition to this, when calling a normal function, you're not giving up the thread your code is executing on, you pass it to the next function you call, and when it returns it gives back the thread. With asynchronous code when an async function is called the execution is at some suspended and you give up the thread to the system that can use that to prioritize other work. When the async function finishes, the system resumes the execution of your code on that thread.

Functions can be marked explicitly as async, indicating that they are asynchronous.  Await marks a potential suspension point in the execution.

Parallel execution without DispatchGroups

Previous versions of Swift required you to use DispatchGroups to be able to execute asynchronous code in parallel. The Swift 5.5 release allows you to call async functions in parallel by simply writing async if from of let when you define a constant and then write await every time you use it.

 1 // Start downloading photos in parallel without any suspension
 2 async let firstPhoto = downloadPhoto(url: urls[0])
 3 async let secondPhoto = downloadPhoto(url: urls[1])
 4 async let thirdPhoto = downloadPhoto(url: urls[2])
 5 
 6 // Suspension occurs when photos need to be used
 7 let album = await [firstPhoto, secondPhoto, thirdPhoto]
 8 return album

The calls to downloadPhoto kick off their work without waiting for the previous ones to complete and run in parallel. We don't mark them method calls with await because we do not want to suspend the execution to wait for the result, we only wait for the results when all three photos have been downloaded.

Asynchronous sequences

Another great feature introduced with the async/await functionality in Swift 5.5 is the ability to iterate over asynchronous sequences of values. What does this do? Take for example reading lines from a CSV file. We do want to read data line by line but what we don't want is to load all that data from disk and store it in memory. Now we can use an AsyncSequence that returns lines one by one when available, like that:

 1 // FileHandle for a CSV life on disk
 2 let csvFileHandle = FileHanle(forReadingFrom: csvFileURL)
 3 for try await line in csvFileHandle.bytes.lines {
 4 	print(line)
 5 }

As one would expect, AsyncSequence supports multiple functions for manipulating sequences such as map, flatMap, compactMap,  reduce, max \ min, zip, filter, and several others. This elevates functional programming to a new level and providing an out-of-the-box alternative to Combine

If you think this is neat, you can also implement your async sequences by conforming to the AsyncSequence and AsyncIteratorProtocol as described in Swift Evolution Proposal SE-0298.

 1 struct Counter : AsyncSequence {
 2   let howHigh: Int
 3 
 4   struct AsyncIterator : AsyncIteratorProtocol {
 5     let howHigh: Int
 6     var current = 1
 7     mutating func next() async -> Int? {
 8       // We could use the `Task` API to check for cancellation here and return early.
 9       guard current <= howHigh else {
10         return nil
11       }
12 
13       let result = current
14       current += 1
15       return result
16     }
17   }
18 
19   func makeAsyncIterator() -> AsyncIterator {
20     return AsyncIterator(howHigh: howHigh)
21   }
22 }
23 
24 for await i in Counter(howHigh: 3) {
25   print(i)
26 }
27 
28 /* 
29 Prints the following, and finishes the loop:
30 1
31 2
32 3
33 */

Asynchronous properties

The Swift 5.5 also comes with the ability to add asynchronous properties to the new concurrency system. In situations where you need to read a property from the Core Data storage or maybe from a file on disk, you can now expose it via an async computed property.

 1 var lastSavedTimestamp: Date {
 2 	get async throws {
 3 		// Gets the lastSavedTimestamp or throws an error
 4 		try await database?.getLastSavedTimestamp()
 5 	}
 6 }

Have you noticed the throws in the getter declaration? This is also a new feature introduced in Swift 5.5. This can be used independently of the async/await features called throwing properties. While this can come in handy in some situations, I would advise against using async properties for long-running network calls.

Actors

One of the nastiest types of bugs you can deal with is data race conditions - memory accessed from multiple threads at the same time such as reading and writing the same property in a multithreaded setting. These issues are hard to identify and even harder to fix.

Swift's actor type is here to fix that. The actor type is a reference type and similar to classes they can have properties and methods. The exception is that actors do not support inheritance and all that comes with it: class members, overriding, convenience, and required initializers. Here's how a thread-safe bank account might look like thanks to actors:

 1 actor BankAccount {
 2 	let owner = "Owner name"
 3 	private(set) var balance: Double = 0.0
 4 
 5 	func add(funds: Double) {
 6 		balance += funds
 7 	}
 8 
 9 	func withdraw(funds: Double) {
10 		balance -= funds
11 	}
12 }
13 
14 let account = BankAccount()
15 print(account.owner)
16 // Prints Owner name
17 await account.add(funds: 100.0)
18 print(await account.balance)
19 // Prints 100

As you can notice, accessing the mutating state requires synchronized access to it while accessing a non-mutating state is thread-safe.

Testing the async code from Swift 5.5

Until now, we could easily test asynchronous code using expectations. Here is how a test would look like when testing the closure version of the previous getLastSavedTimestamp function.

 1 func testLastSavedTimestamp() {
 2 	let expectation = XCTestExpectation(description: "Last saved timestamp is set")
 3 	// Fetch the last saved timestamp
 4 	database?.getLastSavedTimestamp() { (timestamp) in
 5 		// Make sure we the timestamp is present.
 6 		XCTAssertNotNil(timestamp, "No data was downloaded.")    
 7 		expectation.fulfill()
 8   }
 9     
10 	// Wait until the expectation is fulfilled, with a timeout of 10 seconds.
11 	wait(for: [expectation], timeout: 10.0)
12 }

Fortunately, the new async/await features allow us to test this as follows, without waiting an arbitrary number of seconds for the result to come back.

 1 func testLastSavedTimestamp() async throws {
 2 	// Fetch the last saved timestamp
 3 	let lastSavedTimestamp = try await getLastSavedTimestamp()
 4 	XCTAssertNotNil(timestamp, "No data was downloaded.")
 5 }

A note here. XCTAssertThrowsError and other assertions APIs are not yet fully supported so you might have to perform a do catch to test throwing code. 

Using async/await in Swift UI

Swift UI can also make use of the new async/await features but it's not as straightforward as you might expect. Because most of the Swift UI modifiers take plain non-async closure we have to perform some adjustments to make it work.

 1 // Image to be displayed
 2 @State private var image: UIImage?
 3 ...
 4 Image(uiImage: self.image ? placeholderImage)
 5 	.onAppear {
 6 		// This is not possible a onAppear is not an async closure
 7 		self.image = try? await self.viewModel.fetchImage(for: post)
 8 	}

To bridge these async and non-async contexts we need to use the async Task function. This packages work in a closure and sends it to the system for immediate execution on the next available thread.

 1 // Image to be displayed
 2 @State private var image: UIImage?
 3 ...
 4 Image(uiImage: self.image ? placeholderImage)
 5 	.onAppear {
 6 		Task {
 7 			self.image = try? await self.viewModel.fetchImage(for: post)
 8 		}
 9 	}

Final thoughts on async/await in Swift 5.5

The new async/await features are definitely an amazing addition to Swift language and, in my opinion, the long wait was completely justified. Using async/await our code will give a better semantic structure that will be easier to read and write. I, for one, can't wait for the future cancellation and task priority features that should follow this.

While we can write our own async functions, remember that we can also take advantage of these features in the iOS SDK, thanks to the Swift compiler that takes completion handlers through the SKD and exposes them as async functions in Swift 5.5.

Learn from our colleague Ciprian Redinciuc from his on Userdesk.