Reading time: 11 min

Here you can find recipes for common tasks you need when working with the new Swift 5.5 concurrency model.

Where applicable, there's before and after comparison that shows how things where done before async/await (mostly with GCD) and how they can be done now.

This list will be updated as Swift 5.5 gains wider adoption and as more recipes are concocted.

Table of contents

Basic async/await use-case
  1. Get rid of callbacks and the pyramid of doom.
  2. Just return values instead of passing them as callbacks arguments.
  3. Overall make code easier to read, write and reason about.

Before:

func getFileNames(callback: @escaping ([String]) -> Void) {
  // Use GCD to simulate asnyc background work
  DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    callback(["Doc1.txt", "Doc2.txt", "Doc3.txt"])
  }
}

func getFileContent(for files: [String], callback: @escaping (String) -> Void) {
  DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    callback(files.joined(separator: "\n"))
  }
}

func use() {
  getFileNames { fileNames in
    getFileContent(for: fileNames) { content in
      print(content)
    }
  }
}

After:

func getFileNames() async -> [String] {
  ["Doc1.txt", "Doc2.txt", "Doc3.txt"]
}

func getFileContent(for files: [String]) async -> String {
  files.joined(separator: "\n")
}

func use() async {
  let fileNames = await getFileNames()
  let content = await getFileContent(for: fileNames)
  print(content)
}

Tip: You can put before and after code side-by-side, and it'll compile, because the functions don't have the same signature.

Error handling

No more Result, just throw and then try/catch:

Before:

func getFileNames(callback: @escaping (Result<[String], Error>) -> Void) {
  // What a nice, stable function this is
  if Int.random(in: 0...1) == 0 {
    callback(.success(["Doc1.txt", "Doc2.txt", "Doc3.txt"]))
  } else {
    callback(.failure(SomeError.yepAnError))
  }
}

func use() {
  getFileNames { result in
    switch result {
    case .success(let fileNames):
      print(fileNames)
    case .failure(let error):
      print(error.localizedDescription)
    }
  }
}

After:

func getFileNames() async throws -> [String] {
  if Int.random(in: 0...1) == 0 {
    return ["Doc1.txt", "Doc2.txt", "Doc3.txt"]
  } else {
    throw SomeError.yepAnError
  }
}

func use() async {
  do {
    let fileNames = try await getFileNames()
    print(fileNames)
  } catch {
    print(error.localizedDescription)
  }
}

Of course, you can try? or try! as well - all the usual Swift error handling rules apply here as well.

async always goes before throws - but try goes before await.

Sequential execution

Calls to async functions execute sequentially by default, in the order in which they're listed:

// file1 is downloaded first, followed by file2, and then file3
let file1 = await downloadFile("File1.txt")
let file2 = await downloadFile("File2.txt")
let file3 = await downloadFile("File3.txt")

for file in [file1, file2, file3] {
  printFileContent(file)
}

Parallel execution

Use async let to make async calls execute in parallel:

async let file1 = downloadFile("File1.txt")
async let file2 = downloadFile("File2.txt")
async let file3 = downloadFile("File3.txt")

// the parallel downloads start at this `await` call
for file in await [file1, file2, file3] {
  printFileContent(file)
}

Cooperative execution with TaskGroup

If you have multiple tasks (pieces of async code) that work together to produce a result, and which may have a hierarchical ordering to them, wrap them in a TaskGroup. This is, in a way, similar to what you do in Combine with sequences of flatMap and Publishers.Merge:

func getFileNames() async -> [String] {
  return ["Doc1.txt", "Doc2.txt", "Doc3.txt"]
}

func getFileContent(for fileName: String) async -> String {
  await Task.sleep(UInt64.random(in: 1...3) * 1_000_000_000)
  return "Content for \(fileName)"
}

func getAllFiles() async {
  // combine tasks with a TaskGroup
  let allContents = await withTaskGroup(of: String.self, 
                                        returning: String.self) { taskGroup in
    // add tasks to the group
    for fileName in await getFileNames() {
      taskGroup.addTask(priority: nil) { () -> String in
        await getFileContent(for: fileName)
      }
    }

    // collect task results
    var contents = [String]()
    for await content in taskGroup {
      contents.append(content)
    }
    return contents.joined(separator: "\n\n")
  }
  print(allContents)
}

Run async function in sync function

To run some async code in a non-async block of code, wrap it in a Task:

func mySyncFunc() {
  print("Starting async func")
  Task {
    await use()
  }
  print("More sync code")
}

This doesn't make the code run synchronously, though - it just allows you to call async code within sync code. The output of the function above is:

Starting async func
More sync code
Now I'll sleep for 5 seconds...
Up and running again!

The initialized Task runs immediately, so there's no need to kick it off manually.

Wrap sync code with callbacks into async with continuations

If you have a sync function that uses callbacks that you can't directly convert into an async one (because it's in an external library, for compatibility reasons, etc.), you can wrap it with a continuation using someBackgroundTaskWrapper. Once your callback is invoked, resume the execution of the async function by calling resume(returning:) on the continuation parameter:

func someBackgroundTask(callback: @escaping (String) -> Void) {
  callback("Some string")
}

func someBackgroundTaskWrapper() async -> String {
  await withCheckedContinuation { cont in
    someBackgroundTask { result in
      cont.resume(returning: result)
    }
  }
}

If you need to propagate errors from the wrapped sync function, use withCheckedThrowingContinuation and throw errors with resume(throwing:):

func getFileNames(callback: @escaping (Result<[String], Error>) -> Void) {
  // What a nice, stable function this is
  if Int.random(in: 0...1) == 0 {
    callback(.success(["Doc1.txt", "Doc2.txt", "Doc3.txt"]))
  } else {
    callback(.failure(SomeError.yepAnError))
  }
}

func getFileNamesWrapper() async throws -> [String] {
  try await withCheckedThrowingContinuation { cont in
    getFileNames { result in
      switch result {
      case .success(let fileNames):
        cont.resume(returning: fileNames)
      case .failure(let error):
        cont.resume(throwing: error)
      }
    }
  }
}

Continuations can automatically handle Result callbacks using resume(with:), which allows us to write code from above as:

getFileNames { result in
  cont.resume(with: result)
}

Continuations must be resumed, and they must be resumed exactly once.

Pause / delay / sleep an async function

If you want to pause the execution of the current code for a while, use Task.sleep.

Before:

func use() {
  print("Now I'll sleep for 5 seconds...")
  DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    print("Up and running again!")
  }
}

After:

func use() async {
  print("Now I'll sleep for 5 seconds...")
  try? await Task.sleep(5_000_000_000)
  print("Up and running again!")
}

Note that Task.sleep takes in nanoseconds as parameter.

To make specifying sleep/delay intervals easier, add this helpful extension to your code:

extension TimeInterval {
  var toNanoseconds: UInt64 {
    UInt64(self * 1E9)
  }
}

Which allows you to write:

try? await Task.sleep(5.toNanoseconds)
try? await Task.sleep(2.5.toNanoseconds)

Return value from a Task

Task is basically a cancellable async block of code, which means it can return a value:

let task = Task { () -> String in
  print("Doing some work")
  return "Result of my hard work"
}

let taskResult = await task.value
print(taskResult)

You always need to disambiguate the Task block, as Swift compiler can't infer it on its own.

Cancel a Task

Suppose you have a task:

let task = Task { ... }

Cancel it in outside code with:

task.cancel()

Inside the task, handle cancellation with checkCancellation, which then throws a CancellationError:

let task = Task {
  print("Doing work")
  do {
    try Task.checkCancellation() // have I been cancelled in the meantime?
    print("Nope, still going strong!")
  } catch CancellationError {
    print("Cancelled)
  }
}

Or, you can mode the error handling to the call site:

let task = Task { () -> String in
  print("Doing some work")
  try Task.checkCancellation()
  return "Result of my hard work"
}

let taskResult = try? await task.value

Do work on background thread

You shouldn't really think about async/await code in terms of threads, but if it's really important that you move your work off the main thread, use Task(priority:):

func test() async {
  Thread.printCurrent() // runs on main thread

  Task.detached(priority: .background) {
    Thread.printCurrent() // runs off main thread
  }
}

Which results in:

⚡️: <_NSMainThread: 0x600001c980b0>{number = 1, name = main}
🏭: com.apple.main-thread

⚡️: <NSThread: 0x600001c9c790>{number = 7, name = (null)}
🏭: None

You can find the Thread.printCurrent function definition here.

Async computed properties

In Swift 5.5, it's also possible to declare async getters for computed properties:

var myProperty: Int {
  get async {
    try? await Task.sleep(3.toNanoseconds)
    return 5
  }
}

Async setters (set async) aren't supported at the moment.

Async sequence

AsyncSequence is just the same as regular Sequence, except its values can come in asynchronously. It implements an AsyncIteratorProtocol, of which you can think a bit as a Combine Publisher, except this is easier to work with for simpler use cases.

Here's a sample iterator and sequence that simulate delays that might come in due to networking:

struct DelayedStrings: AsyncSequence {
  typealias Element = String

  struct DelayedIterator: AsyncIteratorProtocol {
    private var internalIterator = ["Some", "strings", "here"].makeIterator()

    mutating func next() async -> String? {
      await Task.sleep(2_000_000_000)
      return internalIterator.next()
    }
  }

  func makeAsyncIterator() -> DelayedIterator {
    DelayedIterator()
  }
}

Using for await for iterate an async sequence:

for await value in DelayedStrings() {
  print(Date())
  print(value)
}

which prints:

2021-08-23 08:22:04 +0000
Some
2021-08-23 08:22:07 +0000
strings
2021-08-23 08:22:09 +0000
here

Using other common sequence methods:

print(Date())
let contains = await DelayedStrings().contains(where: { $0 == "here" })
print(contains)
print(Date())

which prints:

2021-08-23 08:31:33 +0000
true
2021-08-23 08:31:39 +0000

Generator

Use AsyncSequence to create a generator - an infinite iterator that produces a new value whenever it's invoked:

struct FibonacciSequence: AsyncSequence {
  typealias Element = Int

  struct FibonacciGenerator: AsyncIteratorProtocol {
    private var a = 0
    private var b = 1

    mutating func next() async -> Int? {
      let c = a
      a = b
      b = a + c
      return c
    }
  }

  func makeAsyncIterator() -> FibonacciGenerator {
    FibonacciGenerator()
  }
}

Async stream

There are two ways of looking at AsyncStream (and its error-handling twin, AsyncThrowingStream):

  1. They serve a similar purpose as continuations do, just for code that produces a sequence of values as opposed to just wrapping a single callback.
  2. They allow for implementing the same functionality as AsyncSequence offers without having to write a custom struct.
func delayedStrings() -> AsyncStream<String> {
  let strings = ["Some", "strings", "here"];
  return AsyncStream { continuation in
    Task {
      for string in strings {
        await Task.sleep(2_000_000_000)
        continuation.yield(string)
      }
      continuation.finish()
    }
  }
}

Then, you can consume it with for await, just like AsyncSequence:

for await value in delayedStrings() {
  print(value)
}

Iterating over Combine Publishers

In Combine, every Publisher has a values property that conforms to AsyncSequence, which means that you can iterate over publishers with for await instead of using sink or custom subscribers:

let publisher = [1, 2, 3, 4, 5]
  .publisher
  .delay(for: .seconds(3), scheduler: RunLoop.main)
  .eraseToAnyPublisher()

for await value in publisher.values {
  print(Date())
  print(value)
}

This, expectedly, outputs:

2021-09-24 08:20:35 +0000
1
2021-09-24 08:20:38 +0000
2
2021-09-24 08:20:41 +0000
3
2021-09-24 08:20:44 +0000
4
2021-09-24 08:20:47 +0000
5

Basic actor

Here's a breakdown of basic actor functionality:

  1. Actors are reference types, like classes.
  2. Actors are thread/task safe.
  3. All actor methods and accessors are async by default, without having to specify it manually. Consequently, all their invocations must be marked with await.
  4. Actor data can only be set/mutated inside the actor itself.
actor StringDatabase {
  private var data = [String]()

  func insert(_ string: String) {
    data.append(string)
  }

  func query() -> [String] {
    data
  }

  func delete(_ string: String) {
    if let index = data.firstIndex(of: string) {
      data.remove(at: index)
    }
  }
}

func use() async {
  let db = StringDatabase()
  await db.insert("A")
  await db.insert("B")
  await print(db.query())
  await db.delete("A")
  await print(db.query())
}

@globalActor

Global actors allow you to mark classes, properties and methods that are supposed to run on the same thread. Conformance to @globalActor requires your type to be a singleton:

@globalActor struct NetworkingActor {
  actor MyActor {
    // custom implementation
  }

  static var shared = MyActor()  // singleton
}

Then, you can mark code elements with it:

class MyViewModel {
  // ...
  @NetworkingActor
  func  loadData() async -> Data {
    // ...
  }
}

@MainActor

@MainActor is a special global actor whose code is run on the main thread. Just like with other global actors, you can mark classes, properties or methods with it to indicate that they should run on the main thread:

```swift
class MyViewModel {
  @MainActor
  func renderData() {
    // ...
  }

  @NetworkingActor
  func  loadData() async -> Data {
    // ...
  }
}

Resolving error: "Property 'X' isolated to global actor 'MainActor' can not be mutated from a non-isolated context"

You may encounter this error if you're changing @State vars inside an async block, e.g, when using the pull-to-refresh compatibility component. The solution is to move the code to a function marked with @MainActor:

@MainActor // HERE
func refreshAction() async {
  await Task.sleep(3_000_000_000)
  now = Date()
}

var body: some View {
  RefreshableScrollView(action: refreshAction) {
    // ...
  }
}

Resolving error: "Converting non-concurrent function value to '@Sendable () async -> Void' may introduce data races"

You may encounter this error if you're changing @State vars inside an async block, which is a parameter of a function, marked with @Sendable in the function signature. The solution is to move the code to a function marked with @Sendable:

@Sendable // HERE
func refreshAction() async {
  await Task.sleep(3_000_000_000)
  now = Date()
}

var body: some View {
  RefreshableScrollView(action: refreshAction) {
    // ...
  }
}

Next Post Previous Post