Getting started with async/await in Swift 5.5

Swift 5.5 introduces async/await functionality

Posted on by Ciprian Redinciuc Ciprian Redinciuc · 7 min read

What is async/await?

Swift 5.5 comes with a lot of new features and the biggest ones without any doubt are the new built-in async/await features for writing asynchronous code. Even though before this it was possible to write asynchronous or parallel code, it was harder to read and reason about.

Thanks to async/await, asynchronous code can be written using a single line of code which makes our code better semantically structured and addresses a lot of the problems of dealing with closures and delegates.

Let's take for example a data task that loads a URL and calls back a completion handler when done.

func startLoading(url: URL, completionHandler: @escaping (String?, Error?) -> Void) { 
	// Create a data task
	let task = URLSession.shared.dataTask(with: url) { data, response, error in
		if let error = error {
			completionHandler(nil, error)
			return 
		}
		// Make sure the HTTPURLResponse is correct
		guard let httpResponse = response as? HTTPURLResponse,
		   (200...299).contains(httpResponse.statusCode) else {
		   completionHandler(nil, error)
		   return
		 }
		 
		 // Parse the response string
		 if let data = data,
			let string = String(data: data, encoding: .utf8) {
			completionHandler(string, nil)
		}
	}

	// Resume the task
	task.resume()
}

This is both hard to follow and prone to mistakes such as forgetting to call the completion handler or forgetting to call return after an early completion handler callback.

In a nutshell we want to do two operations in sequece: fetch some data and transform it to a string. Let's see how this will look with async/await:

func startLoading(url: URL) async throws -> String { 
	// Await the data and HTTPURLResponse
	let (data, response) = try await URLSession.shared.data(for: url)
	
	// Make sure it is the right status code
	guard let httpResponse = response as? HTTPURLResponse,
		(200...299).contains(httpResponse.statusCode) else {
			throw RequestError.badStatusCode
	}
	
	// Parse the response string
	guard let string = String(data: data, encoding: .utf8) else {
		throw BadResponse.invalidFormat
	}
	
	return string
}

The first thing we observe is that we no longer specify a completion handler in our method signature and 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 awaitable 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.

Throwing errors instead of passing back errors in callbacks is so much simpler, easier to understand and implement.


But that's not all, when calling a normal function, you're not giving up the thread your code is executing on, you just pass it to the next function you call and it gives back the thread when it returns. 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 prioritise other work. When the async function has finished, the system resumes the execution of your code on that thread.


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

Parallel execution without DispatchGroups

In previous versions of Swift, you had to use DispatchGroups to be able to execute asynchronous code in parallel. With the 5.5 release of Swift, you are now able 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.

// Start downloading photos in parallel without any suspension
async let firstPhoto = downloadPhoto(url: urls[0])
async let secondPhoto = downloadPhoto(url: urls[1])
async let thirdPhoto = downloadPhoto(url: urls[2])

// Suspension occurs when photos need to be used
let album = await [firstPhoto, secondPhoto, thirdPhoto]
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 nice feature introduced with the async/await functionality is the ability to iterate over asynchronous sequences of values. Take for example reading lines from a CSV file. We want to read data line by line but we don't want to load all that data from disk and store it in memory. What we could do instead now is use an AsyncSequence that returns lines one by one when available:

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

As one would expect, AsyncSequence supports multiple functions for manipulating sequences such as map, compactMap, flatMap, reduce, max \ min, zip, filter and several others. This elevates functional programming to a whole new level and provides 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 the Swift Evolution Proposal SE-0298.

struct Counter : AsyncSequence {
  let howHigh: Int

  struct AsyncIterator : AsyncIteratorProtocol {
    let howHigh: Int
    var current = 1
    mutating func next() async -> Int? {
      // We could use the `Task` API to check for cancellation here and return early.
      guard current <= howHigh else {
        return nil
      }

      let result = current
      current += 1
      return result
    }
  }

  func makeAsyncIterator() -> AsyncIterator {
    return AsyncIterator(howHigh: howHigh)
  }
}

for await i in Counter(howHigh: 3) {
  print(i)
}

/* 
Prints the following, and finishes the loop:
1
2
3
*/

Asynchronous properties

One other nice feature added with the new concurrency system is the abilty to add asynchronous properties. There are situations where you could need to read a property from the Core Data storage or maybe from a file on disk and to do that you can now expose it via a async computed property.

var lastSavedTimestamp: Date {
	get async throws {
		// Gets the lastSavedTimestamp or throws an error
		try await database?.getLastSavedTimestamp()
	}
}

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

Actors

One of the most nasty type of bugs you can deal with are data race conditions - memory accesed 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 comes to fix that. The actor type is a reference type and similar to classes they can have propertie and methods with the exception that actors do not support inheritance and all that comes with that: overidding, class members, convenience and required initializers. Here's how a thread safe bank account might look like thanks to actors:

actor BankAccount {
	let owner = "Owner name"
	private(set) var balance: Double = 0.0

	func add(funds: Double) {
		balance += funds
	}

	func withdraw(funds: Double) {
		balance -= funds
	}
}

let account = BankAccount()
print(account.owner)
// Prints Owner name
await account.add(funds: 100.0)
print(await account.balance)
// Prints 100

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

Testing async code

Up until now, you could easily test asynchronous code by using expectations. Let's see how a test would look for testing the a closure version of the previous getLastSavedTimestamp function.

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

Thanks to the new async/awaint features we can now test this as following, without waiting a arbitrary number of seconds for the result to come back.
func testLastSavedTimestamp() async throws {
	// Fetch the last saved timestamp
	let lastSavedTimestamp = try await getLastSavedTimestamp()
	XCTAssertNotNil(timestamp, "No data was downloaded.")
}

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 all work.

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

To bridge these async and non-async contexts you 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.

// Image to be displayed
@State private var image: UIImage?
...
Image(uiImage: self.image ? placeholderImage)
	.onAppear {
		Task {
			self.image = try? await self.viewModel.fetchImage(for: post)
		}
	}

Conclusion

The new async/await features are an amazing adition to the Swift language and the long wait was completely justified. By using async/await our code will have a better semantic structure and will be easier to read and write and I can't wait for the future cancellation and task priority features that will follow this.

While we can write our own async functions, 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.

Want product news and updates?

Sign up for our newsletter to stay up to date.

We care about the protection of your data. Read our Privacy Policy.

© 2020 Applicodo SRL. All rights reserved.