0

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")
}
Harry J
  • 1,842
  • 3
  • 12
  • 28
  • 1
    "Print statements work but any code falls into the void" suggests that you're calling UI operations from a thread other than the main thread. If print statements are working, but other code isn't, then the code is running. Put a breakpoint in and check which thread you are running from. It isn't clear from the code in your question what you're actually trying to do. – jrturton Jun 13 '22 at 10:06
  • @jrturton I've ensured it's running on the main thread with `@MainActor`. It's purposefully basic to aid any reproducibility. – Harry J Jun 13 '22 at 10:36
  • Reproducing your code in a bare-bones project works just fine, so please include some more details. – jrturton Jun 13 '22 at 10:58
  • 1
    @HarryJ Runs perfectly at my project. Seems like problem is somewhere else and this example code is "too barebone" - and therefore works. Try adding more info and the scope from where you are initializing the `UIBarbuttonItem`. – Mr.SwiftOak Jun 13 '22 at 12:51
  • So I dumbed down the view, and believe I've isolated it to the async function that returns `withCheckedThrowingContinuation`. I'll update the answer with some more code. – Harry J Jun 14 '22 at 00:15

1 Answers1

0

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])
}
Harry J
  • 1,842
  • 3
  • 12
  • 28