Home > Software design >  Swift concurrency failing on first load
Swift concurrency failing on first load

Time:06-15

I have a concurrent function running, that is connected to a UIBarbuttonItem

.init(barButtonSystemItem: .camera, target: self, action: #selector(runClassification(_:))

Although I have run into a weird issue. Running any task (as simple or complex) will not run on the very first initialisation of the view controller. Moving the code outside the Task runs as expected. Running the function, leaving the view, and tapping the button again will successfully disable the buttons.

Print statements work but any code falls into the void. View Controller isn't constructed any differently than usual.

@objc func runClassification(_ sender: UIBarButtonItem) {
    Task {
// This runs
        navigationItem.rightBarButtonItems?.forEach { $0.isEnabled = false }
        guard let image = imageView.image else { return }
        do {
            let response = try await MLClassifer.sharedManager.updateClassifications(capturedImage: image)
// None of this runs on first call.
            print("MLClassifer.sharedManager.updateClassifications begining")
        } catch {
            debugPrint(error)
        }
    }
}

-- Update MLClassifier.swift


private typealias ClassifierCheckedContinuation = CheckedContinuation<[VNRecognizedObjectObservation], Error> // 1
private var classifierContinuation: ClassifierCheckedContinuation?

@MainActor
func updateClassifications(capturedImage: UIImage) async throws -> [VNRecognizedObjectObservation] {
    self.capturedImage = capturedImage
    let orientation = CGImagePropertyOrientation(capturedImage.imageOrientation)
    
    guard let ciImage = CIImage(image: capturedImage) else {
        throw MLClassiferError.invalidConversion
    }
    
    guard let model = self.model else {
        throw MLClassiferError.invalidModel
    }
    
    let mlModel = try VNCoreMLModel(for: model)
    
    let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
    
    let request = VNCoreMLRequest(model: mlModel) { request, error in
        self.processClassifications(for: request, error: error)
    }
    
    try handler.perform([request])
    
    return try await withCheckedThrowingContinuation { (continuation: ClassifierCheckedContinuation) in
        self.classifierContinuation = continuation
    }
}

func processClassifications(for request: VNRequest, error: Error?) {
    if let error = error {
        classifierContinuation?.resume(throwing: error)
        return
    }

    guard let results = request.results, !results.isEmpty else {
        classifierContinuation?.resume(throwing: MLClassiferError.invalidConversion)
        return
    }
        
    guard let classifications = results as? [VNRecognizedObjectObservation], !classifications.isEmpty else {
        classifierContinuation?.resume(throwing: MLClassiferError.invalidConversion)
        return
    }
    
    guard let image = self.capturedImage,
          let capturedImage = CIImage(image: image)
    else {
        classifierContinuation?.resume(throwing: MLClassiferError.invalidConversion)
        return
    }

    classifierContinuation?.resume(returning: classifications)
    print("MLClassifer.sharedManager.updateClassifications returning")
}

CodePudding user response:

Seems to me like problem with converting old escaping closure callback API to async callback with withCheckedThrowingContinuation . I see some that you are not resuming continuation, but rather just assigning some class variables self.classifierContinuation = continuation which is propably like workaround for the function to be returning async throws ->. Are you actually using [VNRecognizedObjectObservation] object that you return from updateClassifications function? If not, then change your updateClassifications likewise to return Void:

@MainActor
func updateClassifications(capturedImage: UIImage) async throws {
    self.capturedImage = capturedImage
    let orientation = CGImagePropertyOrientation(capturedImage.imageOrientation)
    
    guard let ciImage = CIImage(image: capturedImage) else {
        throw MLClassiferError.invalidConversion
    }
    
    guard let model = self.model else {
        throw MLClassiferError.invalidModel
    }
    
    let mlModel = try VNCoreMLModel(for: model)
    
    let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
    
    let request = VNCoreMLRequest(model: mlModel) { request, error in
        self.processClassifications(for: request, error: error)
    }
    
    try handler.perform([request])
    
    return try await withCheckedThrowingContinuation { continuation in
        continuation.resume(returning: () )
        
    }
}

If you are actually using [VNRecognizedObjectObservatio] object you returned from updateClassifications function, then please provide the explanation , where exactly are you returning the object, as it was not clear to me. Also for more information about standard use of withCheckedThrowingContinuation you can read following article . If there is anything unclear or need clarification, do not hesitate to ask.

CodePudding user response:

After some more semi-sleepless nights, I found the solution.

Rather than having it like this;

let request = VNCoreMLRequest(model: mlModel) { request, error in
        self.processClassifications(for: request, error: error)
    }
    
    try handler.perform([request])
    
    return try await withCheckedThrowingContinuation { (continuation: ClassifierCheckedContinuation) in
        self.classifierContinuation = continuation
    }

You need to have it like this: Throwing the handler.perform inside the closure seems to have been the missing puzzle.

return try await withCheckedThrowingContinuation { continuation in
    self.classifierContinuation = continuation
    
    let request = VNCoreMLRequest(model: mlModel) { request, error in
        self.processClassifications(for: request, error: error)
    }

    try? handler.perform([request])
}
  • Related