A few examples of async await in Swift

Async await was added to Swift 5.5 and brought a new way how to write asynchronous code. Functions can be annotated with an async keyword, which enables them to be called with the await keyword. When an async function is being awaited, the execution of the current context is suspended and is resumed when the async function finishes. There are a lot more details in the SE-0296. In this blog post, we’ll take a look at a few examples of async await.

Wrapping completion handler based function

We can add a separate async functions for completion hander code. Which is nice since we do not reimplement everything we have using completion handlers. Take a look at a bit longer explanation in Adding async await to existing completion handler based code.

public func storeData(_ dataProvider: @escaping () -> Data?, identifier: Identifier = UUID().uuidString) async throws -> Identifier {
return try await withCheckedThrowingContinuation({ continuation in
self.storeData(dataProvider, identifier: identifier) { result in
continuation.resume(with: result)
}
})
}
Wrapping a completion handler function which uses the Result type.

Calling async function from non-async context

When using the await keyword then the current asynchronous context suspends until the awaited async function finishes. Suspension can only happen if we are in an asynchronous context. Therefore, a function without the async keyword can’t use await directly. Fortunately, we can create asynchronous contexts easily with a Task. One example of this is when we use MVVM in SwiftUI views and the view model has different methods we want to call when, for example, a user taps on a button

func refreshDocuments() {
Task(priority: .userInitiated) {
do {
self.documents = try await fetcher.fetchAllDocuments()
}
catch {
// Handle error
}
}
}
Creating a Task for enabling to use await on an async function.

Running tasks concurrently

Sometimes we have several independent tasks we need to complete. Let’s take an example when we want to preload messages for conversations. The code snippet below takes an array of conversations and then starts loading messages for each of the conversation. Since conversations are not related to each other, we can do this concurrently. This is what a TaskGroup enables us to do. We create a group and add tasks to it. Tasks in the group can run at the same time, which can be a time-saver.

func preloadMessages(for conversations: [Conversation]) async {
await withThrowingTaskGroup(of: Void.self) { taskGroup in
for conversation in conversations {
taskGroup.addTask {
try await self.preloadMessages(for: conversation)
}
}
}
}

Retrying a task with an exponential delay

This is especially related to networking code, where we might want to retry a couple of times before giving up and displaying an error. Additionally, we might want to wait before each request, and probably we want to wait a bit longer with each delay. Task has a static function detached(priority:operation:) so let’s create a similar retried() static function. In addition to priority and operation, we have arguments for defining how many times to retry and how much to delay, where the delay is increased exponentially with each retry. The first retry attempt is delayed by default 1 second, then the next 2 seconds, the third 4 seconds and so on. If the task happens to be cancelled while waiting, then the Task.sleep(nanoseconds:) throws CancellationError.

extension Task {
static func retried(times: Int, backoff: TimeInterval = 1.0, priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) -> Task where Failure == Error {
Task(priority: priority) {
for attempt in 0..<times {
do {
return try await operation()
}
catch {
let exponentialDelay = UInt64(backoff * pow(2.0, Double(attempt)) * 1_000_000_000)
try await Task<Never, Never>.sleep(nanoseconds: exponentialDelay)
continue
}
}
return try await operation()
}
}
}
Retrying a Task with an exponential delay.

self.documents = try await Task.retried(times: 3) {
try await self.fetcher.fetchAllDocuments() // returns [Documents]
}.value
Example of Task.retried() with accessing the returned value.

If this was helpful, please let me know on Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.