QR code scanning has become a staple feature in modern iOS applications, enabling quick data transfer, seamless authentication, and interactive marketing experiences. Apple's AVFoundation framework offers a robust, low-level API for real-time media capture and processing, including the detection of machine-readable codes like QR codes. This article provides a comprehensive guide to implementing QR code scanning in an iOS app using AVFoundation, covering setup, configuration, delegate handling, UI integration, best practices, and common troubleshooting techniques. By the end, you will have a production-ready scanner that functions reliably across devices and iOS versions.

Understanding AVFoundation for QR Code Detection

AVFoundation is the framework of choice for camera-based features on iOS. It abstracts hardware access and provides a pipeline for capturing video frames, processing metadata, and rendering previews. For QR code scanning, the key components are:

  • AVCaptureSession – the central coordinator that manages data flow from input to output.
  • AVCaptureDeviceInput – wraps the camera (usually the rear camera) as an input source.
  • AVCaptureMetadataOutput – processes frames and extracts machine-readable metadata such as QR codes, barcodes, and faces.
  • AVCaptureVideoPreviewLayer – displays the live camera feed on screen.

A capture session works by connecting one or more inputs to one or more outputs. For QR code scanning, you connect a camera input to a metadata output. The metadata output calls a delegate method each time a code is detected. The entire process runs on background threads, ensuring the UI remains responsive.

Setting Up the Project

Creating a New Xcode Project

Begin by creating a new iOS App project in Xcode. Choose Swift or Objective‑C – the API is identical. For this guide we use Swift.

Adding Camera Usage Permission

iOS requires explicit user permission to access the camera. Add the following key to your Info.plist file:

<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes.</string>

Without this entry, AVCaptureDevice will throw an error and your app may crash. The description string is shown in the system permission dialog.

Checking Camera Availability

Before configuring a capture session, confirm that the device has a camera and that the app is authorized. Use AVCaptureDevice.authorizationStatus(for: .video) and request permission if needed. A best practice is to handle all possible authorization states (notDetermined, restricted, denied, authorized).

Configuring the Capture Session

The capture session is the heart of the scanning process. Below is the typical setup sequence. For clarity, we will present the code in a ViewController that conforms to AVCaptureMetadataOutputObjectsDelegate.

Creating and Configuring the Session

import AVFoundation
import UIKit

class QRScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
    var captureSession: AVCaptureSession!
    var previewLayer: AVCaptureVideoPreviewLayer!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupCaptureSession()
    }

    private func setupCaptureSession() {
        captureSession = AVCaptureSession()
        captureSession.sessionPreset = .high

        // 1. Get the rear camera
        guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
            print("Camera not available")
            return
        }

        // 2. Create input
        let videoInput: AVCaptureDeviceInput
        do {
            videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
        } catch {
            print("Error creating camera input: \(error.localizedDescription)")
            return
        }

        // 3. Add input to session
        guard captureSession.canAddInput(videoInput) else {
            print("Cannot add input")
            return
        }
        captureSession.addInput(videoInput)

        // 4. Create metadata output
        let metadataOutput = AVCaptureMetadataOutput()
        guard captureSession.canAddOutput(metadataOutput) else {
            print("Cannot add metadata output")
            return
        }
        captureSession.addOutput(metadataOutput)

        // 5. Set delegate and queue
        metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
        // 6. Specify QR code type (and optionally other types)
        metadataOutput.metadataObjectTypes = [.qr]

        // 7. Setup preview layer
        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.frame = view.layer.bounds
        previewLayer.videoGravity = .resizeAspectFill
        view.layer.addSublayer(previewLayer)

        // 8. Start session on a background thread
        DispatchQueue.global(qos: .background).async {
            self.captureSession.startRunning()
        }
    }
}

Note: Starting the session on a background thread prevents blocking the main queue during camera initialization. The session runs continuously until you stop it.

Selecting Metadata Object Types

In step 6 we set metadataOutput.metadataObjectTypes = [.qr]. AVFoundation supports many barcode symbologies (EAN‑13, PDF417, Aztec, etc.). If your app should scan multiple types, pass an array like [.qr, .ean13, .code128]. However, be aware that requesting more types may increase detection latency and power consumption.

Implementing the Delegate for QR Code Detection

The delegate method metadataOutput(_:didOutput:from:) is called when the metadata output matches one of the specified types. It passes an array of AVMetadataObject instances. For QR codes, each object is of type AVMetadataMachineReadableCodeObject. Extract the string value and handle it.

func metadataOutput(_ output: AVCaptureMetadataOutput,
                    didOutput metadataObjects: [AVMetadataObject],
                    from connection: AVCaptureConnection) {
    // Stop scanning after first successful detection (optional)
    captureSession.stopRunning()

    // Process metadata objects
    if let metadataObject = metadataObjects.first {
        guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
              let stringValue = readableObject.stringValue else { return }

        // Handle the scanned QR code
        AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
        found(code: stringValue)
    }
}

func found(code: String) {
    // Present the result (e.g., show an alert, open a URL, or store data)
    let alert = UIAlertController(title: "QR Code Scanned",
                                  message: code,
                                  preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
        // Optionally restart scanning
        DispatchQueue.global(qos: .background).async {
            self.captureSession.startRunning()
        }
    })
    present(alert, animated: true)
}

This minimal implementation stops the capture session after the first detection, vibrates the device, and displays the result in an alert. If your app requires continuous scanning (e.g., scanning multiple codes without restarting), remove the stopRunning() call and manage the detection state manually.

Displaying the Camera Preview (AVCaptureVideoPreviewLayer)

The preview layer renders the live video feed from the camera to the screen. In the setup code above, we added it as a sublayer of the view’s main layer. To ensure proper layout, update the preview’s frame in viewDidLayoutSubviews():

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    previewLayer.frame = view.layer.bounds
}

Consider using a custom UIView subclass that hosts the preview layer for better encapsulation. You can also add a translucent overlay (e.g., a corner‑guide view) to indicate where the user should position the QR code.

Converting Coordinates for the Overlay

If you want to highlight the detected QR code with an animation rectangle, use previewLayer.transformedMetadataObject(for:) to convert the metadata object’s coordinates from capture‑device space to view‑layer coordinates.

if let transformedObject = previewLayer.transformedMetadataObject(for: metadataObject) as? AVMetadataMachineReadableCodeObject {
    let bounds = transformedObject.bounds
    // Draw a shape layer matching bounds
}

Handling QR Code Data and User Feedback

QR codes can encode URLs, plain text, contact information (vCard), Wi‑Fi credentials, and more. After extracting the string value, you should validate and react appropriately:

  • URL detection – use URL(string:) and if valid, open it with UIApplication.shared.open(url:).
  • Plain text – display in a label or copy to clipboard.
  • Custom app schemes – parse your own format (e.g., “yourapp://action?param=value”).

Provide immediate haptic feedback via UIImpactFeedbackGenerator and optionally play a short sound. This confirms to the user that the scan succeeded.

Best Practices

Region of Interest

By default, AVFoundation scans the entire camera frame. To improve accuracy and reduce false positives, set the rectOfInterest property on AVCaptureMetadataOutput. The rectangle uses normalized coordinates (0‑1) relative to the camera image (not the preview layer). Coordinating this with the preview layer can be tricky; use metadataOutput.rectOfInterest = previewLayer.metadataOutputRectConverted(fromLayerRect: overlayView.frame).

Torch Control

In low‑light conditions, enable the camera torch. Check if the device has a torch (device.hasTorch) and set try device.lockForConfiguration() then device.torchMode = .on. Remember to unlock after configuration.

Performance Considerations

  • Set sessionPreset to .high or .medium. Higher presets consume more battery but yield better detection for small codes.
  • Use DispatchQueue.global(qos: .userInitiated) for starting the session, and run the delegate queue on the main thread if you update the UI. For heavy processing, consider a custom serial queue.
  • Stop the session when the view disappears (viewWillDisappear) to save resources.

Multiple Code Detection

To scan multiple codes in a single frame without stopping, iterate over metadataObjects and match each readable object. Manage a set of already‑scanned strings to avoid duplicate processing. Use a debounce timer to prevent rapid successive detections of the same code.

Troubleshooting Common Issues

Camera Permission Denied

If the user denies camera access, the capture session will fail to start. Present a dialog explaining why the permission is required and optionally provide a button to open the app’s settings via UIApplication.openSettingsURLString.

Capture Session Not Running

Check that canAddInput and canAddOutput return true. Also ensure the session starts on a background queue. A common mistake is to call startRunning() on the main thread, which can hang the UI.

Metadata Object Types Not Supported

Some old devices or iOS versions may not support certain barcode types. Always verify metadataOutput.availableMetadataObjectTypes before setting the metadataObjectTypes array. Fall back gracefully by showing an error message.

Preview Layer Not Visible

Make sure the preview layer is inserted at the correct index in the view’s layer hierarchy. If you use Auto Layout, update the layer frame in viewDidLayoutSubviews.

Advanced Features: Scanning Multiple Codes, Continuous Scanning, and Custom UI

Continuous Scanning Without Stopping

To allow scanning one code after another without restarting the session, remove the stopRunning() call. Instead, use a flag to ignore repeated detection of the same code within a short time window. For example:

private var lastScannedCode: String?
private var scanTimer: Timer?

func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
    for metadata in metadataObjects {
        guard let readable = metadata as? AVMetadataMachineReadableCodeObject,
              let value = readable.stringValue else { continue }
        if value != lastScannedCode {
            lastScannedCode = value
            // handle code
            // reset timer
            scanTimer?.invalidate()
            scanTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in
                self.lastScannedCode = nil
            }
        }
    }
}

Custom UI Overlays

Create a “scan area” view with transparent center and border guides. Use Core Graphics to draw a rectangle or cross‑hairs. Constrain the overlay to match the rectOfInterest region. Load a design that mimics popular scanner apps (e.g., a white corner bracket).

Handling Different QR Code Data Types

If your app needs to handle structured data like vCards or MeCards, parse the string accordingly. For example, a vCard string starts with “BEGIN:VCARD”. Use CNContactVCardSerialization to create a contact object. For Wi‑Fi credentials, parse the “WIFI:” prefix to extract SSID and password.

External Resources

For further reading, consult the official Apple documentation:

Additionally, the AVCamBarcode sample code provides a complete reference implementation.

Conclusion

Integrating QR code scanning into an iOS app with AVFoundation is a straightforward process when you understand the capture session pipeline. By following the steps outlined in this article – setting up permissions, configuring the session, implementing the metadata output delegate, and displaying the camera preview – you can add reliable code scanning to your app. Remember to handle edge cases like permission denial, device compatibility, and performance optimization. With the advanced tips provided, you can extend the scanner to support continuous scanning, custom UIs, and multiple barcode formats, delivering a polished user experience. Always refer to Apple’s official documentation for the latest API changes and best practices.