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 - Calling async functions with
await
- Use
Task
outside of an async context - Calling async functions in parallel
- URLSession example
- Combining
async
withthrows
- Calling completion block functions from an
async
function - See also
- Further reading
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
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
}
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)!
URLSession.shared.dataTask(with: url) {
data, _, _ in
let games = try! JSONDecoder().decode(
[String].self,
from: data!
)
completion(games)
}.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
func getGames() async throws -> [String] {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://swiftly.dev/api/games")!)
return try JSONDecoder().decode([String].self, from: data)
}
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
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)
}
}
}
let games = await getGames()
print(games)
// Output: ["Backgammon", "Chess", "Go", "Mahjong"]
Using withCheckedThrowingContinuation
import Foundation
func legacyGetGames(completion: @escaping (Result<[String], Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: "https://swiftly.dev/api/games")!) { data, _, _ 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)
}
}
}
}
do {
let games = try await getGames()
print(games)
} catch {
print("Could not get games: \(error.localizedDescription)")
}
// Output: ["Backgammon", "Chess", "Go", "Mahjong"]