A QR code, short for Quick Response code, is a two dimensional barcode. Because it stores information in both the horizontal and vertical direction it can contain more information than a traditional barcode. It can for example store a url which deep links to a certain page in your app.

Because nowadays many people own a smartphone with a build in QR code reader and a fast internet connection they are really taking off. For example in China you can pay anywhere by using a QR code and this is rapidly expanding to other countries in Southeast Asia.

Since iOS 11 the native Camera app has built in support for scanning QR codes, eliminating the need for a separate app. However you can also built this functionality into your own app for example when you want it to be more discoverable or want to provide a custom user interface. This can be easily done with the help of the AVFoundation framework.

Setting up the Capture Session

Everything related to QR code scanning revolves around an AVCaptureSession. A capture session manages access to an AVCaptureDevice, which represents a physical capture device in your phone and connects it to one or multiple AVCaptureOutputs. In our case we will add the default video camera that is located at the back of the phone as the input and as an output we attach an instance of AVCaptureMetadataOutput that will forward us the metadata from the video. Finally, we tell the output the type of metadata were interested in by setting the metadataObjectTypes to [.qr].

func setupCaptureSession() {
    guard let videoCaptureDevice = captureDeviceType.default(for: .video),
        let captureDeviceInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
        captureSession.canAddInput(captureDeviceInput) else {
            //The device is not available or can't be added to the session.
            return
    }

    let metadataOutput = AVCaptureMetadataOutput()
    guard captureSession.canAddOutput(metadataOutput) else {
        // The output can't be added to the session.
        return
    }

    captureSession.addInput(captureDeviceInput)
    captureSession.addOutput(metadataOutput)

    guard metadataOutput.availableMetadataObjectTypes.contains(.qr) else {
        // QR code metadata output is not available on this device
        return
    }

    metadataOutput.metadataObjectTypes = [.qr]
    metadataOutput.setMetadataObjectsDelegate(self, queue: processingQueue)
}

Setting up the Camera

To show the camera video preview in the scanner UI we need to add an instance of AVCaptureVideoPreviewLayer as a sublayer of our view, connect it to the capture session and tell the session to startRunning. Before we do that we need to make sure that we have permission to access the camera. To do this we add the NSCameraUsageDescription key to our app’s Info.plist with a description of why our app needs access to the camera, check the authorization status and respond to it.

func requestAuthorization() {
    switch AVCaptureDevice.authorizationStatus(for: AVMediaType.video) {
    case .authorized:
    // The user has previously granted permission to access the camera.
    case .notDetermined:
        // We have never requested access to the camera before.
        AVCaptureDevice.requestAccess(for: .video) { granted in
            DispatchQueue.main.async {
                guard granted else {
                    // The user denied the camera access request.
                    return
                }

                // The user has granted permission to access the camera.
            }
        }
    case .denied, .restricted:
        // The user either previously denied the access request or the
        // camera is not available due to restrictions.
        return
    }
}

One more thing to understand about AVCaptureSession is that the startRunning method is blocking and the class itself is not thread safe. Thats why we create a serial DispatchQueue and make sure we only access the session on that queue. This way we serialize access to the session and prevent blocking the main thread.

Detecting the QR code

At this point we have configured the capture session, connected it to a video preview layer and handled camera permissions from the user. The last thing we need to do is to conform to the AVCaptureMetaDataOutputObjectsDelegate by implementing the captureOutput:didOutputMetadataObjects:fromConnection:. This method will be called when a QR code is detected. Let’s say we expect the QR code to contain a url which we can use to deep link to a certain page in the app. To achieve this our code would look something like this:

func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        guard let readableObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
            let stringValue = readableObject.stringValue,
            let url = URL(string: stringValue) else do {
                return
        }

        // Check if the url is valid deep link
        // Deep deep link to a certain page into the app.
    }

Conclusion

Thanks to the AVFoundation framework adding a QR code scanner to your app is very straightforward and does not require a lot of work. When you decide to do so remember to always handle the camera permissions and to dispatch all session code that interacts with the session on a serial dispatch queue to avoid threading issues and blocking the main thread.

Contact me on Twitter @kairadiagne if you have any questions, comments or feedback.