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
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Dealing With Continuation Checks ... Again?

You're almost there, but a few things remain to take care of. :]

Open the Xcode debug area to see the app's logs, run and tap Pick Image; this time, however, tap Cancel and see what happens in the logs window.

Continuation Leak For Missed Call

Continuation checks? Again? Didn't you fix this already?

Well, that was a different scenario. Here's what's happening this time.

Once you tap Cancel, ImagePickerService returns an empty UIImage, which causes CoreML to throw an exception, not managed in ImageClassificationService.

Contrary to the previous case, this continuation's resume is never called, and the code therefore never returns.

To fix this, head back to the ImageClassifierService and modify the async wrapper to manage the case where the model throws an exception. To do so, you must check whether the results returned in the completion handler are valid.

Open the ImageClassifierService.swift file and replace the existing code of your async throwing classifyImage(_:) (the one in the extension) with this:

func classifyImage(_ image: UIImage) async throws -> ImageClassifierService.Classification {
  return try await withCheckedThrowingContinuation { continuation in
    classifyImage(image) { result in
      switch result {
      case .success(let classification):
        continuation.resume(returning: classification)
      case .failure(let error):
        continuation.resume(throwing: error)
      }
    }
  }
}

Here you use the additional continuation method resume(throwing:) that throws an exception in the calling method, passing the specified error.

Because the case of returning a Result type is common, Swift also provides a dedicated, more compact instruction, resume(with:) allowing you to reduce what's detailed above to this instead:

func classifyImage(_ image: UIImage) async throws -> ImageClassifierService.Classification {
  return try await withCheckedThrowingContinuation { continuation in
    classifyImage(image) { result in
      continuation.resume(with: result)
    }
  }
}

Gotta love it! Now, build and run and retry the flow where the user cancels picking an image. This time, no warnings will be in the console.

One Final Fix

Although the warning about lacking continuation is gone, some UI weirdness remains. Run the app, pick an image, then try picking another one and tap Cancel on this second image.

As you see, the previous image is deleted, while you might prefer to maintain it if the user already selected one.

The final fix consists of changing the ImagePickerService imagePickerControllerDidCancel(:) delegate method to return nil instead of an empty image.

Open the file ImagePickerService.swift and make the following change.

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
  Logger.main.debug("User canceled picking an image")
  continuation?.resume(returning: nil)
  continuation = nil
}

With this last modification, if the user cancels picking up an image, the pickImage() function of ImagePickerService returns nil, meaning ContentViewModel will skip setting the image and calling classifyImage(_:) at all.

Build and run one last time and verify the bug is gone.

Where to Go From Here?

Well done! You streamlined your code and now have a consistent code style in ContentViewModel.

You started with a ContentViewModel that contained different code styles and had to conform to NSObject due to delegate requirements. Little by little, you refactored this to have a modern and easier-to-follow implementation using the async/await Continuation API.

Specifically, you:

  • Replaced the delegate-based component with an object that wraps the delegate and exposes an async function.
  • Made an async extension for completion handler-based component to allow a gradual rewrite of existing parts of the app.
  • Learned the differences between using Checked and Unsafe continuations and how to handle the corresponding check errors.
  • Were introduced to the types of continuation functions, including async and async throwing.
  • Finally, you saw how to resume the execution using the resume instructions and return a value from a continuation context.

It was a fun run, yet as always, this is just the beginning of the journey. :]

To learn more about the Continuation API and the details of the Swift Concurrency APIs, look at the Modern Concurrency in Swift book.

You can download the complete project using the Download Materials button at the top or bottom of this tutorial.

We hope you enjoyed this tutorial. If you have any questions, suggestions, comments or feedback, please join the forum discussion below!