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.

Leave a rating/review
Download materials
Save for later
Share

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.
Note:
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 App Initial Screen

WhatsThat is an image-classifier app. You pick an image, and it provides an image description in return.

WhatsThat Image Classification

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.

Note:
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 UIKit UIImagePickerController.

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:

  1. You indicate this method uses the async/await API by declaring async on its signature.
  2. The await instruction is known as a “suspension point.” Here, you tell the system to suspend the method when await 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 returning Void.
  • resume(returning:) resumes the execution returning the specified argument.
  • resume(throwing:) resumes the execution throwing an exception and is used for ThrowingContinuation only.
  • resume(with:) resumes the execution passing a Result 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.

Delegate Based Communication

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.

Refactoring delegate based components with continuation

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:

  1. 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 function pickImage() isn’t throwing.
  2. The continuation is saved in the class so you can resume it later, once the delegate returns.
  3. Then, once the user selects an image, the resume is called, passing the image as argument.
  4. 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.