WWDC Notes: Meet async await in Swift

Published on: June 8, 2021

There are tons of async await compatible functions built-in into the SDK. Often with an async version and completion handler based function.

Sync code blocks threads, async code doesn’t

When writing async code with completion handlers you unblock threads but it’s easy to not call your completion handlers. For example when you use a guard let and only return in the else clause. Swift can’t enforce this in the compiler which can lead to subtle bugs.

You can’t throw errors from completion handlers. We usually use Result for this. This adds “ceremony” to our code which isn’t ideal.

Futures can help clean up a callback based flow. But while it’s better and you can’t forget to call completion handlers with that, it’s still not ideal.

Async functions fix this. An async function is suffixed with async:

func doSomething() async

The keyword appears before the return type and before throws:

func doSomething() async throws -> ReturnType

Calling an async function and retrieving its result uses await:

let result = await doSomething()

If the method throws, you use try await:

let result = try await doSomething()

On the line after your async call, the result of doSomething() is available. It was produced without blocking the current thread; execution of your code is paused and the thread is freed up where you write await.

Not having to nest completion closures, call completion handlers, and forward errors makes code much simpler. Errors can be thrown so you don’t need Result.

In addition to functions, properties and initializers can also be async.

var property: Type? {
  get async {
    return await self.computeProperty()
  }
}

If the getter can throw, add throws after the async:

var property: Type? {
  get async throws {
    return await self.computeProperty()
  }
}

We can also use asynchronous sequences. These sequences generate their values asynchronously. We use them like this:

for await value in list {
  let transformed = await transform(value)
  // use transformed
}

There’s a specific session on “Meet AsyncSequence”. There’s also a specific session on Swift’s Structured Concurrency that explains running tasks in parallel for example.

Normal functions calls start, do work, return something. When you call a function, your “source” function gives up control to the called version which will in turn give control back when it’s done. This chain always keeps control of the thread.

In an async function, the function can suspend and allow the system to decide whether it will give control back to the async function by resuming it, or it might let somebody else do work on the thread until it makes sense for the async function to get control back.

Marking something as async or with await doesn’t guarantee that your function will suspend. Just that it might suspend. Or rather, will suspend if needed.

The await keyword signals to the developer that the state of your app can change dramatically while a function is suspended and the thread that the function is on is free to do other work. Swift does not guarantee which thread it will resume your function on. This is an implementation detail that you shouldn’t care about.

Since an async function can suspend, you have to call it from an async context to account for this suspension. Await marks where execution might be suspended. While a function is suspended, other work can be done on the thread (not guaranteed).

XCTest has support for async out of the box. No more need for expectation.

You can test async code by marking the test as async and calling your async work like you would normally, asserting that no errors are thrown for example.

Calling asynchronous code from a SwiftUI view (or non-async context) is done by calling the async task function:

async {
  // call async code
}

This makes it easy to call out into async code from a non-async place.

To learn more:

  • Explore structured concurrency in Swift
  • Discover concurrency in SwiftUI

Getting started is easiest by starting small with some built-in Apple APIs that were converted to be async.

A common pattern to update is one where you have a completion handler. The Swift compiler automatically provides async versions of imported Objective-C code that takes a completion handler.

Some delegate methods take a completion handler that developers must call to communicate an async result. Similar to functions that developers call, these delegate methods are now also async which means we can return values from them or throw errors rather than having to call a completion handler.

There are several sessions about this. The Core Data async/await one is an example of this.

NSAsynchronousFetchRequest fetches objects asynchronously, making it a good candidate to integrate with async/await. This API works with a completion handler.

We can wrap custom async work through continuations. A continuation is used to suspend a function, and resume it when appropriate. We do so through withCheckedThrowingContinuation. If you want a non-throwing version, withThrowingContinuation. You can await a call to these functions to provide a suspension point.

Then you can do whatever you need and call resume(throwing:): or resume(returning:) to resume the code again after having done the work you needed to do.

You must always call resume once. If there are code paths where you don’t call resume, Swift will tell you. Calling resume more than once is a problem and ensures that your code crashes if this happens.

You can store a checked continuation on a class. You can then resume (and nil out) a continuation when needed. This can be used for await a delegate’s response to an action.

Swift concurrency: Behind the scenes explains more about these suspend/resume cycle.

Categories

WWDC21 Notes

Subscribe to my newsletter