Search code examples
swiftscopedispatch-queue

Swift 4: Access Variables from DispatchQueue.main (Scope)


I have a CoreML Image Classification task, that takes the "live stream" from the iOS device's [video] camera and occurs in the background. Once objects have been been identified, and other app logic has occurred, I would like to update the UI's label with some of the data.

Can someone explain how the callout to DispatchQueue.main.asyc(execute: { }) is able to access the variable(s) I have been working with? I think this is essentially a scoping issue?

The code I am currently using:

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {

    processCameraBuffer(sampleBuffer: sampleBuffer)

}

func processCameraBuffer(sampleBuffer: CMSampleBuffer) {

    let coreMLModel = Inceptionv3()

    if let model = try? VNCoreMLModel(for: coreMLModel.model) {
        let request = VNCoreMLRequest(model: model, completionHandler: { (request, error) in
            if let results = request.results as? [VNClassificationObservation] {

                var counter = 0
                var otherVar = 0

                for item in results[0...9] {

                    if item.identifier.contains("something") {
                        print("some app logic goes on here")
                        otherVar += 10 - counter
                    }
                    counter += 1

                }
                switch otherVar {
                case _ where otherVar >= 10:
                    DispatchQueue.main.async(execute: {
                        let displayVarFormatted = String(format: "%.2f", otherVar / 65 * 100)
                        self.labelPrediction.text = "\(counter): \(displayVarFormatted)%"
                    })
                default:
                    DispatchQueue.main.async(execute: {
                        self.labelPrediction.text = "No result!"
                    })
                }
            }
        })

            if let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
                let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
                do {
                    try handler.perform([request])
                } catch {
                    print(error.localizedDescription)
                }
            }
    }
}

Its the self.labelPrediction.text = "" lines inside the switch statement that is causing the issue. This var is always 0 currently.


Solution

  • It's not the matter of DispatchQueue. From processCameraBuffer(sampleBuffer:), your code update your UI before it get result.

    To solve this, you need to use escaping closure. Your function should look like this.

    func processCameraBuffer(sampleBuffer: CMSampleBuffer, completion: @escaping (Int, String) -> Void) {
        // 2.
        let request = VNCoreMLRequest(model: model, completionHandler: { (request, error) in
    
          DispatchQueue.main.async(execute: {
              // 3.
              let displayVarFormatted = String(format: "%.2f", otherVar / 65 * 100)
              completion(counter, displayVarFormatted)
          })
        }
    }
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 
        // 1.
        processCameraBuffer(sampleBuffer) { counter, displayVarFormatted in
            /*
             This Closure will be executed from 
             completion(counter, displayVarFormatted)
            */
            // 4.
            self.labelPrediction.text = "\(counter): \(displayVarFormatted)%"
        }
    }
    

    Scope of variable is not a problem from here. You need to handle asynchronous task.

    1. Capture occurs.
    2. processCameraBuffer is called and VNCoreMLRequest executed.
    3. You will get data and execute processCameraBuffer's completion block, by completion().
    4. Update Label.