Swiftly

Swift 5.7 references for busy coders

async/await

async and await are keywords used to run asynchronous code as if it was synchronous. A function can be marked with async to make it asynchronous, and an asyncronous function can be called with await to halt execution until the asynchronous function returns.

On Apple platforms, async/await requires iOS 13+, macOS Monterey+, watchOS 6+, or tvOS 13+. Apple-provided async/await APIs such as URLSession’s async methods require iOS 15+, but can be backported to iOS 13+. Dispatch remains as an alternative for older platforms.

Declaring async functions

func getGames() async -> [String] {
  let games = // async networking code...
  return games
}

func getPlayers() async -> [String] {
  let players = // async networking code...
  return players
}

func getScores() async -> [Int] {
  let scores = // async networking code...
  return scores
}

Calling async functions with await

// These will execute sequentially. Once getGames() finishes, getPlayers() will start, and so on.

let games = await getGames()
let players = await getPlayers()
let scores = await getScores()

print(games)
print(players)
print(scores)

Use Task outside of an async context

Calling an async function from a synchronous context requires must be done in a Task:

Task {
  let games = await getGames()
  print(games)
}

Calling async functions in parallel

async/await can be combined to call multiple async methods at the same time, and then continue execution when all parallel methods have finished.

// These 3 methods will start at the same time, but may finish at different times.
async let games = getGames()
async let players = getPlayers()
async let scores = getScores()

let everything = await [games, players, scores]
// This line won't execute until all 3 methods above have finished.
print(everything)

URLSession example

The following compares a GET request via URLSession using async/await on the left and using the more traditional closure-based completion block syntax on the right.

Notice how on the left, getGames is executed from top to bottom. In comparison on the right, getGames execution jumps from creating task to calling task.resume(), then jumps back up to execute the completion block.

async/await version

import Foundation
import _Concurrency // If using Playgrounds

func getGames() async -> [String] {
  let urlString = "https://swiftly.dev/api/games"
  let url = URL(string: urlString)!
  let session = URLSession.shared
  let decoder = JSONDecoder()

  let (data, _) = try! await session.data(from: url)
  let games = try! decoder.decode(
    [String].self,
    from: data
  )

  return games
}

Task {
  let games = await getGames()
  print(games)
}

// Output: ["Backgammon", "Chess", "Go", "Mahjong"]

Closure version

import Foundation

func getGames(
  completion: @escaping ([String]) -> Void
) {
  let urlString = "https://swiftly.dev/api/games"
  let url = URL(string: urlString)!
  let session = URLSession.shared
  let decoder = JSONDecoder()

  let task = session.dataTask(with: url) {
    data, _, _ in
    let games = try! decoder.decode(
      [String].self,
      from: data!
    )

    completion(games)
  }

  task.resume()
}

getGames() { games in
  print(games)
}

// Output: ["Backgammon", "Chess", "Go", "Mahjong"]

Combining async with throws

Async functions can still throw errors, but they must be called with try await:

import Foundation
import _Concurrency // If using Playgrounds

func getGames() async throws -> [String] {
  let πŸ“ž = URL(string: "tel:5555555555")!
  let (data, _) = try await URLSession.shared.data(from: πŸ“ž)
  return try JSONDecoder().decode([String].self, from: data)
}

Task {
  do {
    let games = try await getGames()
    print(games)
  } catch {
    print("Could not get games: \(error.localizedDescription)")
  }
}

// Output: Could not get games: unsupported URL

Calling completion block functions from an async function

Using withCheckedContinuation

Legacy functions that still use completion blocks can still be called from async methods using withCheckedContinuation. In this example, continuation.resume(returning:) must be called exactly one time.

import Foundation
import _Concurrency // If using Playgrounds

func legacyGetGames(completion: @escaping ([String]) -> Void) {
  URLSession.shared.dataTask(with: URL(string: "https://swiftly.dev/api/games")!) { data, _, _ in
    let games = try! JSONDecoder().decode([String].self, from: data!)
    completion(games)
  }.resume()
}

func getGames() async -> [String] {
  return await withCheckedContinuation { continuation in
    legacyGetGames() { games in
      continuation.resume(returning: games)
    }
  }
}

Task {
  let games = await getGames()
  print(games)
}

// Output: ["Backgammon", "Chess", "Go", "Mahjong"]

Using withCheckedThrowingContinuation

import Foundation
import _Concurrency // If using Playgrounds

func legacyGetGames(completion: @escaping (Result<[String], Error>) -> Void) {
  let πŸ“ž = URL(string: "tel:5555555555")!
  URLSession.shared.dataTask(with: πŸ“ž) { data, _, error in
    if let error = error {
      completion(.failure(error))
      return
    }
    let games = try! JSONDecoder().decode([String].self, from: data!)
    completion(.success(games))
  }.resume()
}

func getGames() async throws -> [String] {
  return try await withCheckedThrowingContinuation { continuation in
    legacyGetGames() { result in
      switch result {
      case .success(let games): continuation.resume(returning: games)
      case .failure(let error): continuation.resume(throwing: error)
      }
    }
  }
}

Task {
  do {
    let games = try await getGames()
    print(games)
  } catch {
    print("Could not get games: \(error.localizedDescription)")
  }
}

// Output: ["Backgammon", "Chess", "Go", "Mahjong"]

See also

Further reading