How to Scan QR Codes in iOS Using AVFoundation

Scan QR codes in an iOS app using AVCaptureMetadataOutput from the AVFoundation framework. AVCaptureMetadataOutput processes the live camera feed, detects metadata objects such as QR codes, and delivers decoded results through a delegate callback. You configure an AVCaptureSession with a camera input and a metadata output, then present the feed through an AVCaptureVideoPreviewLayer. The delegate method metadataOutput(_:didOutput:from:) fires whenever the camera recognizes a QR code.

Set Up the AVCaptureSession with Camera Input

Create an AVCaptureSession and add a device input from the rear camera.

let captureSession = AVCaptureSession()

guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera,
                                           for: .video,
                                           position: .back) else {
    print("No camera available")
    return
}

guard let input = try? AVCaptureDeviceInput(device: camera) else { return }
captureSession.addInput(input)

AVCaptureDevice.default(_:for:position:) returns the rear wide-angle camera. Use .front for selfie-mode scanning. Call addInput(_:) to attach the device to the session.

Configure AVCaptureMetadataOutput for QR Detection

Add an AVCaptureMetadataOutput to the session and restrict detection to QR codes.

let output = AVCaptureMetadataOutput()
captureSession.addOutput(output)

output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
output.metadataObjectTypes = [.qr]

The metadataObjectTypes property controls which barcode formats the scanner detects. Setting it to [.qr] limits detection to QR codes only. This improves performance compared to enabling all types. Apple requires the delegate queue to be a serial queue, and DispatchQueue.main qualifies.

Display the Camera Feed with AVCaptureVideoPreviewLayer

Create an AVCaptureVideoPreviewLayer and add it as a sublayer of the view.

let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.frame = view.bounds
view.layer.addSublayer(previewLayer)

DispatchQueue.global(qos: .background).async {
    captureSession.startRunning()
}

videoGravity set to .resizeAspectFill ensures edge-to-edge coverage. Start the session on a background queue because startRunning() blocks the calling thread.

Request Camera Permission in Info.plist

Add NSCameraUsageDescription to Info.plist with a string such as "Camera access is required to scan QR codes." iOS crashes at runtime without this key when the app attempts camera access.

Decode the QR Code in the Delegate Callback

Implement AVCaptureMetadataOutputObjectsDelegate to receive decoded QR data.

extension ScannerViewController: AVCaptureMetadataOutputObjectsDelegate {
    func metadataOutput(_ output: AVCaptureMetadataOutput,
                        didOutput metadataObjects: [AVMetadataObject],
                        from connection: AVCaptureConnection) {
        guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
              metadataObject.type == .qr,
              let value = metadataObject.stringValue else { return }

        messageLabel.text = value
        // value contains the decoded URL, text, or other data
    }
}

AVMetadataMachineReadableCodeObject provides the decoded content through its stringValue property. Check the type property to confirm the code is a QR code before processing.

Add a Visual Highlight Box Around Detected Codes

Create a UIView with a green border and position it over the detected QR code.

let highlightView = UIView()
highlightView.layer.borderColor = UIColor.green.cgColor
highlightView.layer.borderWidth = 2
view.addSubview(highlightView)

// In the delegate method
if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject {
    let object = previewLayer.transformedMetadataObject(for: metadataObject)
    highlightView.frame = object?.bounds ?? .zero
}

AVCaptureVideoPreviewLayer.transformedMetadataObject(for:) converts the code's coordinates from camera space to view space. This gives you the exact screen position of the QR code.

Scan Multiple Barcode Types

Set metadataObjectTypes to include additional formats for a universal barcode scanner.

output.metadataObjectTypes = [.qr, .code128, .ean13, .ean8, .pdf417, .aztec, .dataMatrix]

AVFoundation supports QR, UPC-E, Code 39, Code 93, Code 128, EAN-8, EAN-13, Aztec, PDF417, ITF14, Data Matrix, and Interleaved 2 of 5. Include only the formats your app actually needs to maintain performance.

Support Torch for Low-Light Scanning

Toggle the device torch when ambient light is low.

guard let device = AVCaptureDevice.default(.builtInWideAngleCamera,
                                           for: .video,
                                           position: .back),
      device.hasTorch else { return }

try? device.lockForConfiguration()
device.torchMode = .on
device.unlockForConfiguration()

Use AVCaptureDevice.isTorchAvailable to check torch support before enabling it. Lock the device for configuration before changing torch mode and unlock it afterward.

Add a Scan Frame Overlay for Visual Guidance

Draw a transparent rectangle in the center of the preview layer to tell users where to aim the camera.

let scanFrame = UIView()
scanFrame.layer.borderColor = UIColor.white.cgColor
scanFrame.layer.borderWidth = 2
scanFrame.frame = CGRect(x: 40, y: view.center.y - 150,
                         width: view.frame.width - 80, height: 300)
view.addSubview(scanFrame)

A centered scan frame with rounded corners improves scan accuracy. Users naturally align the QR code within the visible boundary.

Prevent Duplicate Scans of the Same Code

Track the last scanned value and ignore repeated detections within a time window.

var lastScannedValue: String?
var lastScanTime: Date?

func metadataOutput(_ output: AVCaptureMetadataOutput,
                    didOutput metadataObjects: [AVMetadataObject],
                    from connection: AVCaptureConnection) {
    guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
          object.type == .qr,
          let value = object.stringValue else { return }

    if value == lastScannedValue,
       let lastTime = lastScanTime,
       Date().timeIntervalSince(lastTime) < 2.0 { return }

    lastScannedValue = value
    lastScanTime = Date()
    processScannedCode(value)
}

This prevents the delegate from firing dozens of times for the same QR code. Adjust the cooldown interval based on your use case. A two-second window works well for most scenarios.

Handle Camera Unavailability Gracefully

Check for camera presence before configuring the capture session. Not all iOS devices have cameras.

guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
    let alert = UIAlertController(title: "No Camera",
                                  message: "This device does not have a camera.",
                                  preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default))
    present(alert, animated: true)
    return
}

Use AVCaptureDevice.DiscoverySession for more granular checks. Some devices have a camera but lack a rear-facing option. Handle front-only configurations by defaulting to .front when .back is unavailable.

Read QR Codes from the Photo Library

Use CIDetector from Core Image to scan QR codes in existing images.

guard let image = UIImage(named: "qrcode"),
      let ciImage = CIImage(image: image) else { return }

let detector = CIDetector(ofType: CIDetectorTypeQRCode,
                          context: nil,
                          options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])

let features = detector?.features(in: ciImage) as? [CIQRCodeFeature]
if let code = features?.first?.messageString {
    print("Decoded: (code)")
}

CIDetector works offline and does not require camera permission. Use this for batch processing or when the user uploads an image from the photo picker.

Best Practices for QR Code Scanning

Minimize the metadata types list to only the formats you need for better performance. Start the capture session on a background queue to prevent UI freezes. Check for camera availability using AVCaptureDevice.DiscoverySession before configuring the session. Provide visual guidance with a scan frame overlay and instructional text like "Align QR code within the frame." Track the last scanned value and ignore duplicates within a two-second window to prevent repeated scans of the same code. Handle the no-camera case on devices like the iPod touch or older iPads by showing an informational alert. Test on a physical device because the simulator lacks real camera hardware. Use AVCaptureSession's sessionPreset property set to .high for a good balance between video quality and scanning performance. Configure the preset before adding inputs or outputs to the session.

Summary

This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.

Frequently Asked Questions

Can I test this on the iOS Simulator? Some features require a physical device with a compatible processor. The article notes specific sections where the Simulator is sufficient and where a real device is required.

What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.

All articles