alex
alex

Reputation: 4914

How to scan for QR codes on button press?

I am using the code provided by https://www.hackingwithswift.com/example-code/media/how-to-scan-a-qr-code to make my own scanning app. But I like my scanning to occur on button press. Now for this I put the viewDidLoad() part from the tutorial into its own function:

func cameraScanningLayer(){
    view.backgroundColor = UIColor.blackColor()
    captureSession = AVCaptureSession()

    let videoCaptureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
    let videoInput: AVCaptureDeviceInput

    do {
        videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
    } catch {
        return
    }

    if (captureSession.canAddInput(videoInput)) {
        captureSession.addInput(videoInput)
    } else {
        failed();
        return;
    }


    let metadataOutput = AVCaptureMetadataOutput()

    if (captureSession.canAddOutput(metadataOutput)) {
        captureSession.addOutput(metadataOutput)

        metadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue())
        // need to scan barcode + QRcode
        metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode,AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypePDF417Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypeCode39Code]
    } else {
        failed()
        return
    }

    // Previewlayer with camera
    previewLayer = AVCaptureVideoPreviewLayer(session: captureSession);
    previewLayer.frame = viewForLayer.bounds;
    previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    viewForLayer.layer.addSublayer(previewLayer);

    captureSession.startRunning();
}

And a button action calls the function:

func buttonScanAction() {
    print("Scan")
    scanEnabled = true // like to use some kind of bool/switch
    self.cameraScanningLayer()
}

The problems I have are:

1) On load the camera is not in view

2) After the button is pressed the camera is in view but it always scans automatically

So I thought of using a global:

var scanEnabled: Bool = false

Then, when the button is clicked, set it to true and the scanning is enabled.

For reference here is a sketch:

enter image description here

EDIT my quick fix which might not be the right way to do it.

I replaced the

 let metadataOutput = AVCaptureMetadataOutput() {...} else {
            failed()
            return
        }

and put it between an if statement

if (scanEnabled == true) {

        let metadataOutput = AVCaptureMetadataOutput()

        if (captureSession.canAddOutput(metadataOutput)) {
            captureSession.addOutput(metadataOutput)

            metadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue())
            // to use them both wwe need to skip AVMetadataObjectTypeQRCode
            metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode,AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypePDF417Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypeCode39Code]
            scanEnabled = false
        } else {
            failed()
            return
        }
        } 

Upvotes: 2

Views: 2776

Answers (2)

yaircarreno
yaircarreno

Reputation: 4257

Thanks @alex for this question. I also use the excellent class created by twostraws (Very useful Paul, thank you so much) and also needed to implement code scan reading with only the action of a button. My solution was the following:

I define metadataOutput as a global variable and only in the button action do I integrate them as a delegate:

var metadataOutput: AVCaptureMetadataOutput!

In viewDidLoad method:

metadataOutput = AVCaptureMetadataOutput()

if (captureSession.canAddOutput(metadataOutput)) {
    captureSession.addOutput(metadataOutput)
    // Was removed this line: metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
    metadataOutput.metadataObjectTypes = [.qr]
} else {
    failed()
    return
}

func buttonScanAction() {
    print("Scan")
    metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
}

When I change my view I stop the camera and remove the delegate like this:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if (captureSession?.isRunning == true) {
        captureSession.stopRunning()
    }
    metadataOutput.setMetadataObjectsDelegate(nil, queue: DispatchQueue.main)
}

Upvotes: 0

TwoStraws
TwoStraws

Reputation: 13127

Author of that tutorial here. My method was to use a dedicated scanning view controller, but I guess you want to unify that with your existing view controller – and that's fine. Both approaches work.

If you want to show the camera interface all the time (even when not actively recognising QR codes) then your plan to use a boolean to track whether scanning is enabled is a good one. My example code has a foundCode() method that gets called, and also calls dismissViewControllerAnimated() when codes are found.

In your version, you need to make foundCode() do all the work of stopping the scane, handling the dismissal, etc. You can then add a check for your scanEnabled boolean in one place.

Something like this ought to do it:

func foundCode(code: String) {
    if scanCode == true {
        print(code)

        captureSession.stopRunning()
        AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
        dismissViewControllerAnimated(true, completion: nil)
    }
}

If you wanted to, you could move the scanCode == true check up to didOutputMetadataObjects to save the unnecessary method call.

Upvotes: 4

Related Questions