Beginner’s Guide to Swift Concurrency Continuations

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 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. 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: “` private func fetchData(url: URL) async throws -> Data { let (data, response) = try await URLSession.shared.data(from: url) 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 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. Lastly, you validate the response and return a Data type as promised by the method signature. 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. 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.html. Replace the content of ImagePickerService.html with this: “` import OSLog import UIKit.UIImage class ImagePickerService: NSObject { private var continuation: CheckedContinuation? func pickImage() async -> UIImage? { return await withCheckedContinuation { continuation in if self.continuation == nil { 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”) continuation?.resume(returning: info[.originalImage] as? UIImage) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { Logger.main.debug(“User canceled picking up photo”) 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 function pickImage() 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. Using Image Picker Service Open ContentViewModel.html, and modify it as follows: Remove the inheritance from NSObject on the ContentViewModel declaration. This isn’t required now that ImagePickerService implements UIImagePickerControllerDelegate. Delete the corresponding extension implementing UIImagePickerControllerDelegate and UINavigationControllerDelegate functions, you can find it under // MARK: – Image Picker Delegate. Again, these aren’t required anymore for the same reason. Then, add a property for the new service named imagePickerService under your noImageCaption and imageClassifierService variables. You’ll end up with these three variables in the top of ContentViewModel: “` private static let noImageCaption = “Select an image to classify” private lazy var imageClassifierService = try? ImageClassifierService() lazy var imagePickerService = ImagePickerService() “` Finally, replace the previous implementation of pickImage() with this one: “` @MainActor func pickImage() { presentImagePicker = true Task(priority: .userInitiated) { let image = await imagePickerService.pickImage() presentImagePicker = false if let image { self.image = image classifyImage(image) } } } “` As pickImage() is a synchronous function, you must use a Task to wrap the asynchronous content. Because you’re dealing with UI here, you create the task with a userInitiated priority. The @MainActor attribute is also required because you’re updating the UI, self.image here. After all the changes, your ContentViewModel should look like this: “` class ContentViewModel: ObservableObject { private static let noImageCaption = “Select an image to classify” private lazy var imageClassifierService = try? ImageClassifierService() lazy…

Source link

Leave a Reply