Rich iOS and macOS experiences often perform asynchronous tasks, like getting data from a remote server. New in Swift 5.5, the async
and await
keywords enable developers to simplify asynchronous code and make it easier to implement asynchronous tasks. This post presents an overview of async
and await
with an example:
- Async
a. Async Throws
b. Async Return Value - Await
a. Await Throws - Async Let
a. Async Await Let - Async Await Example
a. Call Async Function From Non Async Code
b. Async Await vs Closures
Async
In previous versions of Swift, asynchronous programming was often implemented through the use of completion blocks:
// The completion block will be called when
// saveChanges completes its asynchronous logic
func saveChanges(completion: (() -> Void)?) { ... }
// In other code, the saveChanges function would
// be called like this:
saveChanges {
// Handle completion
}
Using the new async
keyword, Swift can mark a function as having asynchronous logic:
// Swift automatically handles asynchronous
// completion of saveChanges
func saveChanges() async { ... }
// In other code, the async saveChanges function
// would be called like this:
await saveChanges()
Async Throws
In previous versions of Swift, some completion blocks returned errors indicating the asynchronous logic in a function could potentially result in an error:
func attachImageToData(
imageResponse: URLResponse,
dataResponse: URLResponse,
completion: ((Error?) -> Void)?
)
Using async
allows the attachImageToData
function to utilize throws
, matching the syntax other non-asynchronous functions use for errors:
func attachImageToData(
imageResponse: URLResponse,
dataResponse: URLResponse) async throws
Async Return Value
In previous versions of Swift, some completion blocks contained other types in addition to a potential error. These other types were often logically the return type of asynchronous logic:
func uploadImage(
image: UIImage,
completion: ((URLResponse?, Error?) -> Void)?
)
func uploadData(
data: Data,
completion: ((URLResponse?, Error?) -> Void)?
)
Using async
allows the uploadImage
and uploadData
functions to utilize the expected return type definition -> URLResponse
, matching the syntax of other non-asynchronous functions:
func uploadImage(image: UIImage)
async throws -> URLResponse
func uploadData(data: Data)
async throws -> URLResponse
Await
The await
keyword is used to tell Swift to wait for completion of an asynchronous function:
// Await completion of saveChanges
await saveChanges()
If multiple async
functions are called using await
, Swift will wait for completion of each function before moving onto the next function:
// Await completion of uploadData
// before moving to saveChanges
await uploadData(data: Data())
// Await completion of saveChanges
// before moving forward
await saveChanges()
Await Throws
Like non-asynchronous functions that can throw, try
can be used for asynchronous functions in combination with await
to call the asynchronous function and throw if an error occurs:
try await attachImageToData(
imageResponse: imageResponse,
dataResponse: dataResponse
)
Async Let
In some cases, async
logic requires the return value from an asynchronous function. The async let
syntax can be used to mark a let
variable as asynchronous, meaning the let
variable will be available after some asynchronous logic completes:
async let imageResponse = try uploadImage(
image: image
)
Async Await Let
Combining multiple async let
statements will enable Swift to perform asynchronous logic in parallel:
// Both imageResponse and dataResponse will
// be obtained in parallel
async let imageResponse = try uploadImage(
image: image
)
async let dataResponse = try uploadData(
data: data
)
To wait for completion of previous async let
variables, use await
when the let
variable is required to have been initialized:
try await attachImageToData(
imageResponse: await imageResponse,
dataResponse: await dataResponse
)
Alternatively, use await
to control concurrency and get only one value at a time:
// By using await, uploadImage
// will be called first
let imageResponse = try await uploadImage(
image: image
)
// By using await on uploadImage,
// uploadData will be called after
// imageResponse is initialized
let dataResponse = try await uploadData(
data: data
)
Async Await Example
The async await example presented in this post will reference the following async
functions:
func uploadImage(image: UIImage)
async -> URLResponse
func uploadData(data: Data)
async throws -> URLResponse
func attachImageToData(
imageResponse: URLResponse,
dataResponse: URLResponse) async throws
func saveChanges() async throws
Async await example combining multiple async
calls and return values:
// Define an asynchronous function uploadTask that
// will contain multiple async function calls
func uploadTask(
image: UIImage,
data: Data) async throws {
// Perform the following async functions
// at the same time
async let imageResponse = try uploadImage(
image: image
)
async let dataResponse = try uploadData(
data: data
)
// Wait for completion of attachImageToData
// before moving forward
try await attachImageToData(
imageResponse: await imageResponse,
dataResponse: await dataResponse
)
// Wait for completion of saveChanges
// before moving forward
try await saveChanges()
}
Call Async Function From Non Async Code
Initialize a Task
to make calls to async
functions from non-async code:
func userTappedUpload() {
Task(priority: .default) {
do {
try await self.upload(
image: UIImage(),
data: Data()
)
}
catch {
// Handle error
}
}
}
Async Await vs Closures
One benefit of using async
and await
is easier to read Swift code. If the async
function uploadTask
presented in this post is rewritten to only use completion closures, the function has a deep nested callback structure that is hard to read:
func uploadTask(
image: UIImage,
data: Data,
completion: ((Error?) -> Void)?) {
uploadImage(image: image) { imageResponse in
// Verify imageResponse is successful
self.uploadData(data: data) {
dataResponse, dataError in
// Handle dataError if dataError != nil
self.attachImageToData(
imageResponse: imageResponse!,
dataResponse: dataResponse!) {
attachError in
// Handle attachError if
// attachError != nil
self.saveChanges() {
completion?(nil)
}
}
}
}
}
// Example calling upload
upload(image: UIImage(), data: Data()) { error in
if let error = error {
// Handle error
}
}
Asynchronous Programming In Swift
That’s it! By using async
and await
you can implement asynchronous tasks with callbacks and asynchronous logic in Swift.