[ad_1]
With the introduction of Swift Concurrency and the async/await API, Apple drastically improved the method of writing asynchronous code in Swift. In addition they launched the Continuation API, which you should utilize instead of delegates and completion callbacks. Studying and utilizing these APIs drastically streamlines your code.
You’ll study all in regards to the Continuation API on this tutorial. Particularly, you’ll replace the tutorial app, WhatsThat, to make use of the Continuation API as an alternative of legacy patterns. You’ll study the next alongside the way in which:
- What the Continuation API is and the way it works.
- Tips on how to wrap a delegate-based API part and supply an async interface for it.
- Tips on how to present an async API by way of an extension for parts that use completion callbacks.
- Tips on how to use the async API instead of legacy patterns.
Though not strictly required for this tutorial, confidence with the Swift async/await API will aid you higher perceive how the API works beneath the hood. Our e book, Fashionable Concurrency in Swift, is a good place to start out.
Getting Began
Obtain the starter mission by clicking Obtain Supplies on the prime or backside of this tutorial.
Open WhatsThat from the starter folder, and construct and run.
WhatsThat is an image-classifier app. You decide a picture, and it supplies a picture description in return.
Right here above is Zohar, a beloved Brittany Spaniel — in keeping with the classifier mannequin :]
The app makes use of one of many normal CoreML neural fashions to find out the picture’s most important topic. Nonetheless, the mannequin’s willpower may very well be incorrect, so it additionally offers a detection accuracy share. The upper the proportion, the extra probably the mannequin believes its prediction is correct.
Picture classification is a big matter, however you don’t want to totally perceive it for this tutorial. If wish to study extra, seek advice from Create ML Tutorial: Getting Began.
You may both use the default photos, or you may drag and drop your individual photographs into the simulator’s Photographs app. Both means, you’ll see the obtainable photos in WhatsThat’s picture picker.
Check out the mission file hierarchy, and also you’ll discover these core information:
-
AppMain.swift
launches the SwiftUI interface. -
Display
is a gaggle containing three SwiftUI views. -
ContentView.swift
accommodates the primary app display screen. -
ImageView.swift
defines the picture view utilized in the primary display screen. -
ImagePickerView.swift
is a SwiftUI wrapper round a UIKitUIImagePickerController
.
The Continuation API
As a quick refresher, Swift Concurrency permits you to add async
to a way signature and name await
to deal with asynchronous code. For instance, you may write an asynchronous networking methodology like this:
// 1
non-public func fetchData(url: URL) async throws -> Knowledge {
// 2
let (information, response) = attempt await URLSession.shared.information(from: url)
// 3
guard let response = response as? HTTPURLResponse, response.isOk else {
throw URLError(.badServerResponse)
}
return information
}
Right here’s how this works:
- You point out this methodology makes use of the async/await API by declaring
async
on its signature. - The
await
instruction is called a “suspension level.” Right here, you inform the system to droop the strategy whenawait
is encountered and start downloading information on a special thread.
Swift shops the state of the present operate in a heap, making a “continuation.” Right here, as soon as URLSession
finishes downloading the information, the continuation is resumed, and the execution continues from the place it was stopped.
response
and return a Knowledge
sort as promised by the strategy signature.When working with async/await, the system routinely manages continuations for you. As a result of Swift, and UIKit specifically, closely use delegates and completion callbacks, Apple launched the Continuation API that can assist you transition present code utilizing an async interface. Let’s go over how this works intimately.
Suspending The Execution
SE-0300: Continuations for interfacing async duties with synchronous code defines 4 totally different features to droop the execution and create a continuation.
withCheckedContinuation(_:)
withCheckedThrowingContinuation(_:)
withUnsafeContinuation(_:)
withUnsafeThrowingContinuation(_:)
As you may see, the framework supplies two variants of APIs of the identical features.
-
with*Continuation
supplies a non-throwing context continuation -
with*ThrowingContinuation
additionally permits throwing exceptions within the continuations
The distinction between Checked
and Unsafe
lies in how the API verifies correct use of the resume operate. You’ll find out about this later, so preserve studying… ;]
Resuming The Execution
To renew the execution, you’re alleged to name the continuation offered by the operate above as soon as, and solely as soon as, through the use of one of many following continuation
features:
-
resume()
resumes the execution with out returning a outcome, e.g. for an async operate returningVoid
. -
resume(returning:)
resumes the execution returning the desired argument. -
resume(throwing:)
resumes the execution throwing an exception and is used forThrowingContinuation
solely. -
resume(with:)
resumes the execution passing aEnd result
object.
Okay, that’s sufficient for concept! Let’s bounce proper into utilizing the Continuation API.
Changing Delegate-Based mostly APIs with Continuation
You’ll first wrap a delegate-based API and supply an async interface for it.
Have a look at the UIImagePickerController
part from Apple. To deal with the asynchronicity of the interface, you set a delegate, current the picture picker after which await the person to choose a picture or cancel. When the person selects a picture, the framework informs the app by way of its delegate callback.
Regardless that Apple now supplies the PhotosPickerUI
SwiftUI part, offering an async interface to UIImagePickerController
continues to be related. For instance, you might have to help an older iOS or could have personalized the stream with a particular picker design you wish to keep.
The concept is so as to add a wrapper object that implements the UIImagePickerController
delegate interface on one aspect and presents the async API to exterior callers.
Hiya Picture Picker Service
Add a brand new file to the Companies group and title it ImagePickerService.swift.
Substitute the content material of ImagePickerService.swift
with this:
import OSLog
import UIKit.UIImage
class ImagePickerService: NSObject {
non-public var continuation: CheckedContinuation<UIImage?, By no means>?
func pickImage() async -> UIImage? {
// 1
return await withCheckedContinuation { continuation in
if self.continuation == nil {
// 2
self.continuation = continuation
}
}
}
}
// MARK: - Picture Picker Delegate
extension ImagePickerService: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo data: [UIImagePickerController.InfoKey: Any]
) {
Logger.most important.debug("Person picked picture")
// 3
continuation?.resume(returning: data[.originalImage] as? UIImage)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
Logger.most important.debug("Person canceled choosing up picture")
// 4
continuation?.resume(returning: UIImage())
}
}
First, you’ll discover the pickImage()
operate is async as a result of it wants to attend for customers to pick a picture, and as soon as they do, return it.
Subsequent are these 4 factors of curiosity:
- On hitting
withCheckedContinuation
the execution is suspended, and a continuation is created and handed to the completion handler. On this state of affairs, you employ the non-throwing variant as a result of the async operatepickImage()
isn’t throwing. - The
continuation
is saved within the class so you may resume it later, as soon as the delegate returns. - Then, as soon as the person selects a picture, the
resume
is named, passing the picture as argument. - If the person cancels choosing a picture, you come an empty picture — not less than for now.
As soon as the execution is resumed, the picture returned from the continuation is returned to the caller of the pickImage()
operate.
Utilizing Picture Picker Service
Open ContentViewModel.swift
, and modify it as follows:
- Take away the inheritance from
NSObject
on theContentViewModel
declaration. This isn’t required now thatImagePickerService
implementsUIImagePickerControllerDelegate
. - Delete the corresponding extension implementing
UIImagePickerControllerDelegate
andUINavigationControllerDelegate
features, yow will discover it beneath// MARK: - Picture Picker Delegate
. Once more, these aren't required anymore for a similar motive.
Then, add a property for the brand new service named imagePickerService
beneath your noImageCaption
and imageClassifierService
variables. You may find yourself with these three variables within the prime of ContentViewModel
:
non-public static let noImageCaption = "Choose a picture to categorise"
non-public lazy var imageClassifierService = attempt? ImageClassifierService()
lazy var imagePickerService = ImagePickerService()
Lastly, substitute the earlier implementation of pickImage()
with this one:
@MainActor
func pickImage() {
presentImagePicker = true
Job(precedence: .userInitiated) {
let picture = await imagePickerService.pickImage()
presentImagePicker = false
if let picture {
self.picture = picture
classifyImage(picture)
}
}
}
As pickImage()
is a synchronous operate, it’s essential to use a Job
to wrap the asynchronous content material. Since you’re coping with UI right here, you create the duty with a userInitiated
precedence.
The @MainActor
attribute can be required since you’re updating the UI, self.picture
right here.
After all of the adjustments, your ContentViewModel
ought to seem like this:
class ContentViewModel: ObservableObject {
non-public static let noImageCaption = "Choose a picture to categorise"
non-public lazy var imageClassifierService = attempt? ImageClassifierService()
lazy var imagePickerService = ImagePickerService()
@Printed var presentImagePicker = false
@Printed non-public(set) var picture: UIImage?
@Printed non-public(set) var caption = noImageCaption
@MainActor
func pickImage() {
presentImagePicker = true
Job(precedence: .userInitiated) {
let picture = await imagePickerService.pickImage()
presentImagePicker = false
if let picture {
self.picture = picture
classifyImage(picture)
}
}
}
non-public func classifyImage(_ picture: UIImage) {
caption = "Classifying..."
guard let imageClassifierService else {
Logger.most important.error("Picture classification service lacking!")
caption = "Error initializing Neural Mannequin"
return
}
DispatchQueue.world(qos: .userInteractive).async {
imageClassifierService.classifyImage(picture) { lead to
let caption: String
swap outcome {
case .success(let classification):
let description = classification.description
Logger.most important.debug("Picture classification outcome: (description)")
caption = description
case .failure(let error):
Logger.most important.error(
"Picture classification failed with: (error.localizedDescription)"
)
caption = "Picture classification error"
}
DispatchQueue.most important.async {
self.caption = caption
}
}
}
}
}
Lastly, you might want to change the UIImagePickerController
‘s delegate in ContentView.swift to level to the brand new delegate.
To take action, substitute the .sheet
with this:
.sheet(isPresented: $contentViewModel.presentImagePicker) {
ImagePickerView(delegate: contentViewModel.imagePickerService)
}
Construct and run. It is best to see the picture picker working as earlier than, but it surely now makes use of a contemporary syntax that is simpler to learn.
Continuation Checks
Sadly, there’s an error within the code above!
Open the Xcode Debug pane window and run the app.
Now, decide a picture, and it’s best to see the corresponding classification. While you faucet Choose Picture once more to choose one other picture, Xcode offers the next error:
Swift prints this error as a result of the app is reusing a continuation already used for the primary picture, and the usual explicitly forbids this! Keep in mind, it’s essential to use a continuation as soon as, and solely as soon as.
When utilizing the Checked
continuation, the compiler provides code to implement this rule. When utilizing the Unsafe
APIs and also you name the resume greater than as soon as, nevertheless, the app will crash! In case you neglect to name it in any respect, the operate by no means resumes.
Though there should not be a noticeable overhead when utilizing the Checked
API, it is definitely worth the value for the added security. As a default, choose to make use of the Checked
API. If you wish to eliminate the runtime checks, use the Checked
continuation throughout growth after which swap to the Unsafe
when delivery the app.
Open ImagePickerService.swift, and you may see the pickImage
now appears like this:
func pickImage() async -> UIImage? {
return await withCheckedContinuation { continuation in
if self.continuation == nil {
self.continuation = continuation
}
}
}
You’ll want to make two adjustments to repair the error herein.
First, at all times assign the handed continuation, so you might want to take away the if
assertion, ensuing on this:
func pickImage() async -> UIImage? {
await withCheckedContinuation { continuation in
self.continuation = continuation
}
}
Second, set the set the continuation to nil
after utilizing it:
func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo data: [UIImagePickerController.InfoKey: Any]
) {
Logger.most important.debug("Person picked picture")
continuation?.resume(returning: data[.originalImage] as? UIImage)
// Reset continuation to nil
continuation = nil
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
Logger.most important.debug("Person canceled choosing up picture")
continuation?.resume(returning: UIImage())
// Reset continuation to nil
continuation = nil
}
Construct and run and confirm you can decide as many photos as you want with out hitting any continuation-leak error.
Changing Callback-Based mostly APIs with Continuation
Time to maneuver on and modernize the remaining a part of ContentViewModel
by changing the completion handler within the classifyImage(:)
operate with a sleeker async name.
As you probably did for refactoring UIImagePickerController
, you will create a wrapper part that wraps the ImageClassifierService
and exposes an async API to ContentViewModel
.
On this case, although, you can too lengthen the ImageClassifier
itself with an async extension.
Open ImageClassifierService.swift and add the next code on the finish:
// MARK: - Async/Await API
extension ImageClassifierService {
func classifyImage(_ picture: UIImage) async throws -> ImageClassifierService.Classification {
// 1
return attempt await withCheckedThrowingContinuation { continuation in
// 2
classifyImage(picture) { lead to
// 3
if case let .success(classification) = outcome {
continuation.resume(returning: classification)
return
}
}
}
}
}
Here is a rundown of the code:
- As within the earlier case, the system blocks the execution on hitting the
await withCheckedThrowingContinuation
. - You needn’t retailer the continuation as within the earlier case since you’ll use it within the completion handler. Simply name the previous callback-based API and await the outcome.
- As soon as the part invokes the completion callback, you name
continuation.resume<(returning:)
passing again the classification obtained.
Including an extension to the previous interface permits use of the 2 APIs concurrently. For instance, you can begin writing new code utilizing the async/await API with out having to rewrite present code that also makes use of the completion callback API.
You utilize a Throwing
continuation to mirror that the ImageClassifierService
can throw an exception if one thing goes improper.
Utilizing Async ClassifyImage
Now that ImageClassifierService
helps async/await, it is time to substitute the previous implementation and simplify the code. Open ContentViewModel.swift and alter the classifyImage(_:)
operate to this:
@MainActor
non-public func classifyImage(_ picture: UIImage) async {
guard let imageClassifierService else {
Logger.most important.error("Picture classification service lacking!")
caption = "Error initializing Neural Mannequin"
return
}
do {
// 1
let classification = attempt await imageClassifierService.classifyImage(picture)
// 2
let classificationDescription = classification.description
Logger.most important.debug(
"Picture classification outcome: (classificationDescription)"
)
// 3
caption = classificationDescription
} catch let error {
Logger.most important.error(
"Picture classification failed with: (error.localizedDescription)"
)
caption = "Picture classification error"
}
}
Here is what is going on on:
- You now name the
ImageClassifierService.classifyImage(_:)
operate asynchronously, which means the execution will pause till the mannequin has analyzed the picture. - As soon as that occurs, the operate will resume utilizing the continuation to the code under the
await.
- When you may have a classification, you should utilize that to replace
caption
with the classification outcome.
Notice: In an actual app, you’d additionally wish to intercept any throwing exceptions at this stage and replace the picture caption with an error message if the classification fails.
There’s one last change earlier than you are prepared to check the brand new code. Since classifyImage(_:)
is now an async
operate, you might want to name it utilizing await
.
Nonetheless in ContentViewModel.swift, within the pickImage
operate, add the await
key phrase earlier than calling the classifyImage(_:)
operate.
@MainActor
func pickImage() {
presentImagePicker = true
Job(precedence: .userInitiated) {
let picture = await imagePickerService.pickImage()
presentImagePicker = false
if let picture {
self.picture = picture
await classifyImage(picture)
}
}
}
Since you’re already in a Job
context, you may name the async operate instantly.
Now construct and run, attempt choosing a picture yet another time, and confirm that every part works as earlier than.
Dealing With Continuation Checks … Once more?
You are virtually there, however just a few issues stay to handle. :]
Open the Xcode debug space to see the app’s logs, run and faucet Choose Picture; this time, nevertheless, faucet Cancel and see what occurs within the logs window.
Continuation checks? Once more? Did not you repair this already?
Effectively, that was a special state of affairs. Here is what’s taking place this time.
When you faucet Cancel, ImagePickerService
returns an empty UIImage
, which causes CoreML to throw an exception, not managed in ImageClassificationService
.
Opposite to the earlier case, this continuation’s resume
isn’t referred to as, and the code subsequently by no means returns.
To repair this, head again to the ImageClassifierService
and modify the async wrapper to handle the case the place the mannequin throws an exception. To take action, it’s essential to test whether or not the outcomes returned within the completion handler are legitimate.
Open the ImageClassifierService.swift file and substitute the prevailing code of your async throwing classifyImage(_:)
(the one within the extension) with this:
func classifyImage(_ picture: UIImage) async throws -> ImageClassifierService.Classification {
return attempt await withCheckedThrowingContinuation { continuation in
classifyImage(picture) { lead to
swap outcome {
case .success(let classification):
continuation.resume(returning: classification)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
Right here you employ the extra continuation methodology resume(throwing:)
that throws an exception within the calling methodology, passing the desired error
.
As a result of the case of returning a End result
sort is widespread, Swift additionally supplies a devoted, extra compact instruction, resume(with:)
permitting you to cut back what’s detailed above to this as an alternative:
func classifyImage(_ picture: UIImage) async throws -> ImageClassifierService.Classification {
return attempt await withCheckedThrowingContinuation { continuation in
classifyImage(picture) { lead to
continuation.resume(with: outcome)
}
}
}
Gotta like it! Now, construct and run and retry the stream the place the person cancels choosing a picture. This time, no warnings shall be within the console.
One Closing Repair
Though the warning about missing continuation is gone, some UI weirdness stays. Run the app, decide a picture, then attempt choosing one other one and faucet Cancel on this second picture.
As you see, the earlier picture is deleted, when you may choose to keep up it if the person already chosen one.
The ultimate repair consists of adjusting the ImagePickerService
imagePickerControllerDidCancel(:)
delegate methodology to return nil
as an alternative of an empty picture.
Open the file ImagePickerService.swift and make the next change.
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
Logger.most important.debug("Person canceled choosing a picture")
continuation?.resume(returning: nil)
continuation = nil
}
With this final modification, if the person cancels choosing up a picture, the pickImage()
operate of ImagePickerService
returns nil, which means ContentViewModel
will skip setting the picture and calling classifyImage(_:)
in any respect.
Construct and run one final time and confirm the bug is gone.
The place to Go From Right here?
Effectively performed! You streamlined your code and now have a constant code model in ContentViewModel
.
You began with a ContentViewModel
that contained totally different code types and needed to conform to NSObject
attributable to delegate necessities. Little by little, you refactored this to have a contemporary and easier-to-follow implementation utilizing the async/await Continuation API.
Particularly, you:
- Changed the delegate-based part with an object that wraps the delegate and exposes an async operate.
- Made an async extension for completion handler-based part to permit a gradual rewrite of present elements of the app.
- Discovered the variations between utilizing
Checked
andUnsafe
continuations and methods to deal with the corresponding test errors. - Have been launched to the kinds of continuation features, together with async and async throwing.
- Lastly, you noticed methods to resume the execution utilizing the
resume
directions and return a worth from a continuation context.
It was a enjoyable run, but as at all times, that is only the start of the journey. :]
To study extra in regards to the Continuation API and the main points of the Swift Concurrency APIs, take a look at the Fashionable Concurrency in Swift e book.
You may obtain the entire mission utilizing the Obtain Supplies button on the prime or backside of this tutorial.
We hope you loved this tutorial. In case you have any questions, strategies, feedback or suggestions, please be part of the discussion board dialogue under!
[ad_2]