Wednesday, June 7, 2023
HomeiOS DevelopmentSwift Concurrency Continuations: Getting Began

Swift Concurrency Continuations: Getting Began


Apple drastically improved methods to write asynchronous code in Swift with the introduction of Swift Concurrency and the async/await API. Additionally they launched the Continuation API, and you should utilize this instead of delegates and completion callbacks. You possibly can drastically streamline your code by mastering and utilizing this API.

You’ll be taught all concerning the Continuation API on this tutorial. Particularly, you’ll replace the tutorial app WhatsApp to make use of the Continuation API as a substitute of legacy patterns. You’ll be taught the next alongside the best way:

  • What the Continuation API is and the way it works
  • The way to wrap a delegate-based API element and supply an async interface for it
  • The way to present an async API through an extension for elements that use completion callbacks
  • The way to use the async API instead of legacy patterns
Though not strictly required for this tutorial, confidence with the Swift async/await API will assist you higher perceive how the API works beneath the hood. Discover all of the sources on the Kodeco web site.

Getting Began

Obtain the starter undertaking by clicking the Obtain Supplies button on the prime or backside of this tutorial.

Open WhatsThat from the starter folder, and construct and run.

WhatsThat App Initial Screen

WhatsThat is an image-classifier app. You decide a picture, and it supplies a picture description in return.

WhatsThat Image Classification

Right here above is Zohar, beloved Brittany Spaniel — based on the classifier mannequin :]

The app makes use of one of many customary CoreML neural fashions to find out the picture’s most important topic. Nevertheless, the mannequin’s dedication could possibly be incorrect, so it additionally provides a detection accuracy proportion. The upper the share, the extra seemingly the mannequin believes its prediction is correct.

You possibly can both use the default photographs, or you’ll be able to drag-and-drop your personal pictures into the simulator’s Pictures app. Both method, you’ll see the out there photographs in WhatsThat’s picture picker.

Check out the undertaking 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 principle app display.
  • ImageView.swift defines the picture view utilized in the principle display.
  • ImagePickerView.swift is a SwiftUI wrapper round a UIKit UIImagePickerController.

The Continuation API

As a quick refresher, Swift Concurrency lets you add async to a way signature and name await to deal with asynchronous code. For instance, you’ll be able to write an asynchronous networking methodology like this:


// 1
personal func fetchData(url: URL) async throws -> Knowledge {

  // 2
  let (information, response) = strive 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:

  1. You point out this methodology makes use of the async/await API by declaring async on its signature.
  2. The await instruction is named a “suspension level.” Right here, you inform the system to droop the tactic when await is encountered and start downloading information on a unique thread.

Swift shops the state of the present perform in a heap, making a “continuation.” Right here, as soon as URLSession finishes downloading the info, the continuation is resumed, and the execution continues from the place it was stopped.

  • Lastly, you validate the response and return a Knowledge kind as promised by the tactic 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 current 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 completely different features to droop the execution and create a continuation.

    • withCheckedContinuation(_:)
    • withCheckedThrowingContinuation(_:)
    • withUnsafeContinuation(_:)
    • withUnsafeThrowingContinuation(_:)

    As you’ll be able to 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 perform. You’ll study this later, so preserve studying… ;]

    Resuming The Execution

    To renew the execution, you’re speculated to name the continuation supplied by the perform 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 perform returning Void.
    • resume(returning:) resumes the execution returning the desired argument.
    • resume(throwing:) resumes the execution throwing an exception and is used for ThrowingContinuation solely.
    • resume(with:) resumes the execution passing a End result object.

    Okay, that’s sufficient for concept! Let’s soar proper into utilizing the Continuation API.

    Changing Delegate-Primarily based APIs with Continuation

    You’ll first wrap a delegate-based API and supply an async interface for it.

    Have a look at the UIImagePickerController element from Apple. To deal with the asynchronicity of the interface, you set a delegate, current the picture picker after which await the consumer to select a picture or cancel. When the consumer selects a picture, the framework informs the app through its delegate callback.

    Delegate Based Communication

    Despite the fact that Apple now supplies the PhotosPickerUI SwiftUI element, offering an async interface to UIImagePickerController continues to be related. For instance, it’s possible you’ll have to help an older iOS or might have custom-made the circulation with a particular picker design you wish to keep.

    The thought is so as to add a wrapper object that implements the UIImagePickerController delegate interface on one facet and presents the async API to exterior callers.

    Refactoring delegate based components with continuation

    Howdy Picture Picker Service

    Add a brand new file to the Companies group and identify it ImagePickerService.swift.

    Exchange the content material of ImagePickerService.swift with this:

    
    import OSLog
    import UIKit.UIImage
    
    class ImagePickerService: NSObject {
      personal 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 information: [UIImagePickerController.InfoKey: Any]
      ) {
        Logger.most important.debug("Person picked picture")
        // 3
        continuation?.resume(returning: information[.originalImage] as? UIImage)
      }
    
      func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        Logger.most important.debug("Person canceled selecting up picture")
        // 4
        continuation?.resume(returning: UIImage())
      }
    }
    

    First, you’ll discover the pickImage() perform 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:

    1. On hitting withCheckedContinuation the execution is suspended, and a continuation is created and handed to the completion handler. On this state of affairs, you utilize the non-throwing variant as a result of the async perform pickImage() isn’t throwing.
    2. The continuation is saved within the class so you’ll be able to resume it later, as soon as the delegate returns.
    3. Then, as soon as the consumer selects a picture, the resume is named, passing the picture as argument.
    4. If the consumer cancels selecting a picture, you come back an empty picture — a minimum of for now.

    As soon as the execution is resumed, the picture returned from the continuation is returned to the caller of the pickImage() perform.

    Utilizing Picture Picker Service

    Open ContentViewModel.swift, and modify it as follows:

    1. Take away the inheritance from NSObject on the ContentViewModel declaration. This isn’t required now that ImagePickerService implements UIImagePickerControllerDelegate.
    2. Delete the corresponding extension implementing UIImagePickerControllerDelegate and UINavigationControllerDelegate features, you will discover it beneath // MARK: - Picture Picker Delegate. Once more, these aren't required anymore for a similar cause.

    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:

    
    personal static let noImageCaption = "Choose a picture to categorise"
    personal lazy var imageClassifierService = strive? ImageClassifierService()
    lazy var imagePickerService = ImagePickerService()
    

    Lastly, change the earlier implementation of pickImage() with this one:

    
    @MainActor
    func pickImage() {
      presentImagePicker = true
    
      Process(precedence: .userInitiated) {
        let picture = await imagePickerService.pickImage()
        presentImagePicker = false
    
        if let picture {
          self.picture = picture
          classifyImage(picture)
        }
      }
    }
    

    As pickImage() is a synchronous perform, you should use a Process 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 also be required since you’re updating the UI, self.picture right here.

    After all of the adjustments, your ContentViewModel ought to appear like this:

    
    class ContentViewModel: ObservableObject {
      personal static let noImageCaption = "Choose a picture to categorise"
      personal lazy var imageClassifierService = strive? ImageClassifierService()
      lazy var imagePickerService = ImagePickerService()
    
      @Printed var presentImagePicker = false
      @Printed personal(set) var picture: UIImage?
      @Printed personal(set) var caption = noImageCaption
    
      @MainActor
      func pickImage() {
        presentImagePicker = true
    
        Process(precedence: .userInitiated) {
          let picture = await imagePickerService.pickImage()
          presentImagePicker = false
    
          if let picture {
            self.picture = picture
            classifyImage(picture)
          }
        }
      }
    
      personal 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.international(qos: .userInteractive).async {
          imageClassifierService.classifyImage(picture) { end in
            let caption: String
            change 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, it’s worthwhile to change the UIImagePickerController‘s delegate in ContentView.swift to level to the brand new delegate.

    To take action, change 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, however it 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 you must see the corresponding classification. Once you faucet Choose Picture once more to select one other picture, Xcode provides the next error:

    Continuation Leak For Reuse

    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, you should 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! If you happen to overlook to name it in any respect, the perform 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 do away with the runtime checks, use the Checked continuation throughout improvement after which change to the Unsafe when delivery the app.

    Open ImagePickerService.swift, and you may see the pickImage now seems like this:

    
    func pickImage() async -> UIImage? {
      return await withCheckedContinuation { continuation in
        if self.continuation == nil {
          self.continuation = continuation
        }
      }
    }
    

    It’s worthwhile to make two adjustments to repair the error herein.

    First, all the time assign the handed continuation, so it’s worthwhile 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 information: [UIImagePickerController.InfoKey: Any]
    ) {
      Logger.most important.debug("Person picked picture")
      continuation?.resume(returning: information[.originalImage] as? UIImage)
      // Reset continuation to nil
      continuation = nil
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
      Logger.most important.debug("Person canceled selecting up picture")
      continuation?.resume(returning: UIImage())
      // Reset continuation to nil
      continuation = nil
    }
    

    Construct and run and confirm which you could decide as many photographs as you want with out hitting any continuation-leak error.

    Changing Callback-Primarily based APIs with Continuation

    Time to maneuver on and modernize the remaining a part of ContentViewModel by changing the completion handler within the classifyImage(:) perform with a sleeker async name.

    As you probably did for refactoring UIImagePickerController, you may create a wrapper element that wraps the ImageClassifierService and exposes an async API to ContentViewModel.

    On this case, although, you may as well prolong 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 strive await withCheckedThrowingContinuation { continuation in
          // 2
          classifyImage(picture) { end in
            // 3
            if case let .success(classification) = outcome {
              continuation.resume(returning: classification)
              return
            }
          }
        }
      }
    }
    

    This is a rundown of the code:

    1. As within the earlier case, the system blocks the execution on hitting the await withCheckedThrowingContinuation.
    2. 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.
    3. As soon as the element invokes the completion callback, you name continuation.resume<(returning:) passing again the classification acquired.

    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 current code that also makes use of the completion callback API.

    You employ a Throwing continuation to replicate 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 change the previous implementation and simplify the code. Open ContentViewModel.swift and alter the classifyImage(_:) perform to this:

    
    @MainActor
    personal 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 = strive 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"
      }
    }
    

    This is what is going on on:

    1. You now name the ImageClassifierService.classifyImage(_:) perform asynchronously, which means the execution will pause till the mannequin has analyzed the picture.
    2. As soon as that occurs, the perform will resume utilizing the continuation to the code under the await.
    3. When you will have a classification, you should utilize that to replace caption with the classification outcome.

    Observe: 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 perform, it’s worthwhile to name it utilizing await.

    Nonetheless in ContentViewModel.swift, within the pickImage perform, add the await key phrase earlier than calling the classifyImage(_:) perform.

    
    @MainActor
    func pickImage() {
      presentImagePicker = true
    
      Process(precedence: .userInitiated) {
        let picture = await imagePickerService.pickImage()
        presentImagePicker = false
    
        if let picture {
          self.picture = picture
          await classifyImage(picture)
        }
      }
    }
    

    Since you’re already in a Process context, you’ll be able to name the async perform straight.

    Now construct and run, strive selecting a picture yet one more time, and confirm that the whole lot works as earlier than.

    Dealing With Continuation Checks … Once more?

    You are virtually there, however just a few issues stay to deal with. :]

    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 Leak For Missed Call

    Continuation checks? Once more? Did not you repair this already?

    Nicely, that was a unique state of affairs. This is what’s occurring 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 is rarely 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, you should examine whether or not the outcomes returned within the completion handler are legitimate.

    Open the ImageClassifierService.swift file and change the present code of your async throwing classifyImage(_:) (the one within the extension) with this:

    
    func classifyImage(_ picture: UIImage) async throws -> ImageClassifierService.Classification {
      return strive await withCheckedThrowingContinuation { continuation in
        classifyImage(picture) { end in
          change outcome {
          case .success(let classification):
            continuation.resume(returning: classification)
          case .failure(let error):
            continuation.resume(throwing: error)
          }
        }
      }
    }
    

    Right here you utilize 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 kind is widespread, Swift additionally supplies a devoted, extra compact instruction, resume(with:) permitting you to cut back what’s detailed above to this as a substitute:

    
    func classifyImage(_ picture: UIImage) async throws -> ImageClassifierService.Classification {
      return strive await withCheckedThrowingContinuation { continuation in
        classifyImage(picture) { end in
          continuation.resume(with: outcome)
        }
      }
    }
    

    Gotta find it irresistible! Now, construct and run and retry the circulation the place the consumer cancels selecting a picture. This time, no warnings can be within the console.

    One Ultimate Repair

    Though the warning about missing continuation is gone, some UI weirdness stays. Run the app, decide a picture, then strive selecting one other one and faucet Cancel on this second picture.

    As you see, the earlier picture is deleted, whilst you may choose to take care of it if the consumer already chosen one.

    The ultimate repair consists of fixing the ImagePickerService imagePickerControllerDidCancel(:) delegate methodology to return nil as a substitute of an empty picture.

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

    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
      Logger.most important.debug("Person canceled selecting a picture")
      continuation?.resume(returning: nil)
      continuation = nil
    }

    With this final modification, if the consumer cancels selecting up a picture, the pickImage() perform 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?

    Nicely carried out! You streamlined your code and now have a constant code fashion in ContentViewModel.

    You began with a ContentViewModel that contained completely different code types and needed to conform to NSObject on account of 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 element with an object that wraps the delegate and exposes an async perform.
    • Made an async extension for completion handler-based element to permit a gradual rewrite of current elements of the app.
    • Discovered the variations between utilizing Checked and Unsafe continuations and methods to deal with the corresponding examine errors.
    • Had been launched to the forms 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 all the time, that is only the start of the journey. :]

    To be taught extra concerning the Continuation API and the main points of the Swift Concurrency APIs, take a look at the Fashionable Concurrency in Swift guide.

    You possibly can obtain the entire undertaking utilizing the Obtain Supplies button on the prime or backside of this tutorial.

    We hope you loved this tutorial. If in case you have any questions, recommendations, feedback or suggestions, please be part of the discussion board dialogue under!



    Supply hyperlink

    RELATED ARTICLES

    LEAVE A REPLY

    Please enter your comment!
    Please enter your name here

    - Advertisment -
    Google search engine

    Most Popular

    Recent Comments