Swift Concurrency Continuations: Getting Started
Continuations are a powerful part of Swift Concurrency that helps you to convert asynchronous code using delegates and callbacks into code that uses async/await calls, which is exactly what you will do in this article! By Alessandro Di Nepi.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Swift Concurrency Continuations: Getting Started
25 mins
- Getting Started
- The Continuation API
- Suspending The Execution
- Resuming The Execution
- Replacing Delegate-Based APIs with Continuation
- Hello Image Picker Service
- Using Image Picker Service
- Continuation Checks
- Replacing Callback-Based APIs with Continuation
- Using Async ClassifyImage
- Dealing With Continuation Checks ... Again?
- One Final Fix
- Where to Go From Here?
With the introduction of Swift Concurrency and the async/await API, Apple greatly improved the process of writing asynchronous code in Swift. They also introduced the Continuation API, which you can use in place of delegates and completion callbacks. Learning and using these APIs greatly streamlines your code.
You’ll learn all about the Continuation API in this tutorial. Specifically, you’ll update the tutorial app, WhatsThat, to use the Continuation API instead of legacy patterns. You’ll learn the following along the way:
- What the Continuation API is and how it works.
- How to wrap a delegate-based API component and provide an async interface for it.
- How to provide an async API via an extension for components that use completion callbacks.
- How to use the async API in place of legacy patterns.
Although not strictly required for this tutorial, confidence with the Swift async/await API will help you better understand how the API works under the hood. Our book, Modern Concurrency in Swift, is a great place to start.
Getting Started
Download the starter project by clicking Download Materials at the top or bottom of this tutorial.
Open WhatsThat from the starter folder, and build and run.
WhatsThat is an image-classifier app. You pick an image, and it provides an image description in return.
Here above is Zohar, a beloved Brittany Spaniel — according to the classifier model :]
The app uses one of the standard CoreML neural models to determine the image’s main subject. However, the model’s determination could be incorrect, so it also gives a detection accuracy percentage. The higher the percentage, the more likely the model believes its prediction is accurate.
Image classification is a huge topic, but you don’t need to fully understand it for this tutorial. If want to learn more, refer to Create ML Tutorial: Getting Started.
You can either use the default images, or you can drag and drop your own photos into the simulator’s Photos app. Either way, you’ll see the available images in WhatsThat’s image picker.
Take a look at the project file hierarchy, and you’ll find these core files:
-
AppMain.swift
launches the SwiftUI interface. -
Screen
is a group containing three SwiftUI views. -
ContentView.swift
contains the main app screen. -
ImageView.swift
defines the image view used in the main screen. -
ImagePickerView.swift
is a SwiftUI wrapper around a UIKitUIImagePickerController
.
The Continuation API
As a brief refresher, Swift Concurrency allows you to add async
to a method signature and call await
to handle asynchronous code. For example, you can write an asynchronous networking method like this:
// 1
private func fetchData(url: URL) async throws -> Data {
// 2
let (data, response) = try await URLSession.shared.data(from: url)
// 3
guard let response = response as? HTTPURLResponse, response.isOk else {
throw URLError(.badServerResponse)
}
return data
}
Here’s how this works:
- You indicate this method uses the async/await API by declaring
async
on its signature. - The
await
instruction is known as a “suspension point.” Here, you tell the system to suspend the method whenawait
is encountered and begin downloading data on a different thread.
Swift stores the state of the current function in a heap, creating a “continuation.” Here, once URLSession
finishes downloading the data, the continuation is resumed, and the execution continues from where it was stopped.
When working with async/await, the system automatically manages continuations for you. Because Swift, and UIKit in particular, heavily use delegates and completion callbacks, Apple introduced the Continuation API to help you transition existing code using an async interface. Let’s go over how this works in detail.
Suspending The Execution
SE-0300: Continuations for interfacing async tasks with synchronous code defines four different functions to suspend the execution and create a continuation.
withCheckedContinuation(_:)
withCheckedThrowingContinuation(_:)
withUnsafeContinuation(_:)
withUnsafeThrowingContinuation(_:)
As you can see, the framework provides two variants of APIs of the same functions.
-
with*Continuation
provides a non-throwing context continuation -
with*ThrowingContinuation
also allows throwing exceptions in the continuations
The difference between Checked
and Unsafe
lies in how the API verifies proper use of the resume function. You’ll learn about this later, so keep reading… ;]
Resuming The Execution
To resume the execution, you’re supposed to call the continuation provided by the function above once, and only once, by using one of the following continuation
functions:
-
resume()
resumes the execution without returning a result, e.g. for an async function returningVoid
. -
resume(returning:)
resumes the execution returning the specified argument. -
resume(throwing:)
resumes the execution throwing an exception and is used forThrowingContinuation
only. -
resume(with:)
resumes the execution passing aResult
object.
Okay, that’s enough for theory! Let’s jump right into using the Continuation API.
Replacing Delegate-Based APIs with Continuation
You’ll first wrap a delegate-based API and provide an async interface for it.
Look at the UIImagePickerController
component from Apple. To cope with the asynchronicity of the interface, you set a delegate, present the image picker and then wait for the user to pick an image or cancel. When the user selects an image, the framework informs the app via its delegate callback.
Even though Apple now provides the PhotosPickerUI
SwiftUI component, providing an async interface to UIImagePickerController
is still relevant. For example, you may need to support an older iOS or may have customized the flow with a specific picker design you want to maintain.
The idea is to add a wrapper object that implements the UIImagePickerController
delegate interface on one side and presents the async API to external callers.
Hello Image Picker Service
Add a new file to the Services group and name it ImagePickerService.swift.
Replace the content of ImagePickerService.swift
with this:
import OSLog
import UIKit.UIImage
class ImagePickerService: NSObject {
private var continuation: CheckedContinuation<UIImage?, Never>?
func pickImage() async -> UIImage? {
// 1
return await withCheckedContinuation { continuation in
if self.continuation == nil {
// 2
self.continuation = continuation
}
}
}
}
// MARK: - Image Picker Delegate
extension ImagePickerService: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
Logger.main.debug("User picked photo")
// 3
continuation?.resume(returning: info[.originalImage] as? UIImage)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
Logger.main.debug("User canceled picking up photo")
// 4
continuation?.resume(returning: UIImage())
}
}
First, you’ll notice the pickImage()
function is async because it needs to wait for users to select an image, and once they do, return it.
Next are these four points of interest:
- On hitting
withCheckedContinuation
the execution is suspended, and a continuation is created and passed to the completion handler. In this scenario, you use the non-throwing variant because the async functionpickImage()
isn’t throwing. - The
continuation
is saved in the class so you can resume it later, once the delegate returns. - Then, once the user selects an image, the
resume
is called, passing the image as argument. - If the user cancels picking an image, you return an empty image — at least for now.
Once the execution is resumed, the image returned from the continuation is returned to the caller of the pickImage()
function.