Async/Await in Swift
Starting with Swift 5.5, we were introduced to yet another, though helpful, way to write and perform asynchronous code using Async/Await. For anyone who has worked in JavaScript or C# before, this may seem familiar and welcomed. What Async/Await does is allow us to write async functions just like we would any other synchronous code, and then call them using the await
keyword.
In this recipe, we will take our PhotobookCreator
app and swap out how we use Dispatch for Async/Await, highlighting some of the advantages it brings to Swift.
Getting ready
We will see how we can improve the responsiveness, readability, and safety of an app using Async/Await, so we will work with an improved version of our app. Go to https://github.com/PacktPublishing/Swift-Cookbook-Third-Edition/tree/main/Chapter%206/PhotobookCreator_AsyncAwait. Here, you will find the repository of an app that takes a collection of photos and turns them into a PDF photo book, but using Async/Await. You can download the app source files directly from GitHub or by using git
:
git clone https://github.com/PacktPublishing/Swift-Cookbook-Third-Edition/tree/main/Chapter%206/PhotobookCreator_AsyncAwait/PhotobookCreator
How to do it...
While using Dispatch, we were able to achieve asynchronous code, but there were a few inconveniences. We had to create DispatchQueue
, pass in a completion handler, and ensure that we returned to the main queue to run our completion.
As we’ll see, Async/Await handles some of those details for us and makes much of what we’re trying to achieve more readable:
- First, in
PhotoCollectionViewController
, let’s refactorgeneratePhotoBook
. Instead of taking a closure, let’s return what we would’ve passed into the handler: a URL. To signal that we’ll be running this asynchronously, let’s use theasync
keyword:func generatePhotoBook(with photos: [UIImage]) async -> URL { //... }
- Now, we’ll refactor the body of our function. Since
async
functions are made to look like synchronous functions, you’ll notice it reads very straightforward. However, we’ll add theawait
keyword in front of the calls we expect to be waiting on. Lastly, we’ll simply return the URL we’re looking for:func generatePhotoBook(with photos: [UIImage]) async -> URL { let resizer = PhotoResizer() let builder = PhotoBookBuilder() // Get smallest common size let size = await resizer.smallestCommonSize(for: photos) // Scale down (can take a while) var photosForBook = await resizer.scaleWithAspectFill(photos, to: size) // Crop (can take a while) photosForBook = await resizer.centerCrop(photosForBook, to: size) // Generate PDF (can take a while) let photobookURL = await builder.buildPhotobook(with: photosForBook) return photobookURL }
- The compiler will start complaining that we’re awaiting non-async functions. Let’s fix this by simply adding the
async
keyword to each function we plan to call asynchronously:func smallestCommonSize(for photos: [UIImage]) async -> CGSize func scaleWithAspectFill(_ photos: [UIImage], to size: CGSize) async -> [UIImage] func centerCrop(_ photos: [UIImage], to size: CGSize) async -> [UIImage] func buildPhotobook(with photos: [UIImage]) async -> URL
- Lastly, we update our caller,
generateButtonPressed
. Since it isIBAction
, which is synchronous, we cannot use theasync
keyword. Instead, we’ll wrap our asynchronous code using aTask
closure:@IBAction func generateButtonPressed(sender: UIBarButtonItem) { activityIndicator.startAnimating() Task { let photobookURL = await generatePhotoBook(with: photos) activityIndicator.stopAnimating() let previewController = UIDocumentInteractionController(url: photobookURL) previewController.delegate = self _ = previewController.presentPreview( animated: true) } }
How it works...
A huge advantage of using Async/Await is that it allows us to clearly mark code that will be used asynchronously (using async
) and that we must wait for before moving on (using await
).
This is exceptionally helpful when looking at the body of generatePhotoBook
. First, there’s no need for any closure since the entire function has been marked as asynchronous. Next, we can clearly see the steps our code will be executing by taking note of where await
appears. Lastly, we simply return the value our caller wants to use.
Returning a value instead of calling a closure (specifically on the main thread as we do using Dispatch
) introduces not only better readability but also safety. It puts the responsibility of the callers’ code back where it belongs: in the hands of the caller itself.
We achieve this in generateBackButtonPressed
. Yes, we still end up using a closure, specifically a Task
closure. However, we use it to simply signify that we want to execute an asynchronous call, as opposed to packaging up and sending off our code to be called and executed elsewhere:
Task { let photobookURL = await generatePhotoBook(with: photos) activityIndicator.stopAnimating() let previewController = UIDocumentInteractionController(url: photobookURL) previewController.delegate = self _ = previewController.presentPreview(animated: true) }
We simply await the call to generatePhotoBook
, soundly trusting that once it completes execution, we can safely move forward with updating our UI with a URL.
The syntax changes between using Dispatch and Async/Await provide a dramatic difference in readability, simplicity, and safety, even in the simple use case we used them in. We’ve only scratched the surface of the capabilities offered by Async/Await.
See also
Documentation relating to Async/Await: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/