```html SwiftUI: How to Implement Barcode Scanner (iOS 17+, 2026)

How to implement a barcode scanner in SwiftUI

iOS 17+ Xcode 16+ Advanced APIs: VisionKit / DataScannerViewController Updated: May 12, 2026
TL;DR

Wrap DataScannerViewController from VisionKit inside a UIViewControllerRepresentable, pass it a .barcode recognizer type, and forward scanned payloads to a SwiftUI @Binding via its delegate.

import VisionKit

struct BarcodeScannerView: UIViewControllerRepresentable {
    @Binding var scannedCode: String?

    func makeUIViewController(context: Context) -> DataScannerViewController {
        let scanner = DataScannerViewController(
            recognizedDataTypes: [.barcode()],
            isHighlightingEnabled: true
        )
        scanner.delegate = context.coordinator
        try? scanner.startScanning()
        return scanner
    }

    func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {}

    func makeCoordinator() -> Coordinator { Coordinator(scannedCode: $scannedCode) }

    class Coordinator: NSObject, DataScannerViewControllerDelegate {
        @Binding var scannedCode: String?
        init(scannedCode: Binding<String?>) { _scannedCode = scannedCode }

        func dataScanner(_ dataScanner: DataScannerViewController,
                         didAdd items: [RecognizedItem], allItems: [RecognizedItem]) {
            if case .barcode(let barcode) = items.first {
                scannedCode = barcode.payloadStringValue
            }
        }
    }
}

Full implementation

The complete solution adds availability checking (some simulators and older devices do not support DataScannerViewController), a configurable symbology filter, a result sheet, and proper camera-usage permission handling via Info.plist. The scanner is presented modally from a parent view so the live camera feed does not run in the background unnecessarily. A ScannerViewModel keeps SwiftUI state clean and isolated from the UIKit bridge.

import SwiftUI
import VisionKit

// MARK: - ViewModel

@Observable
final class ScannerViewModel {
    var scannedCode: String?
    var isScannerPresented = false
    var errorMessage: String?

    var isAvailable: Bool {
        DataScannerViewController.isSupported && DataScannerViewController.isAvailable
    }

    func presentScanner() {
        guard isAvailable else {
            errorMessage = "Barcode scanning is not available on this device."
            return
        }
        isScannerPresented = true
    }

    func handleScan(_ code: String?) {
        scannedCode = code
        isScannerPresented = false
    }
}

// MARK: - UIViewControllerRepresentable

struct BarcodeScannerView: UIViewControllerRepresentable {
    @Binding var scannedCode: String?
    var symbologies: [VNBarcodeSymbology] = []   // empty = all types

    func makeUIViewController(context: Context) -> DataScannerViewController {
        let recognizer: Set<DataScannerViewController.RecognizedDataType> = [
            symbologies.isEmpty ? .barcode() : .barcode(symbologies: symbologies)
        ]
        let scanner = DataScannerViewController(
            recognizedDataTypes: recognizer,
            qualityLevel: .balanced,
            recognizesMultipleItems: false,
            isHighlightingEnabled: true
        )
        scanner.delegate = context.coordinator
        try? scanner.startScanning()
        return scanner
    }

    func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(scannedCode: $scannedCode)
    }

    // MARK: Coordinator

    final class Coordinator: NSObject, DataScannerViewControllerDelegate {
        @Binding var scannedCode: String?

        init(scannedCode: Binding<String?>) {
            _scannedCode = scannedCode
        }

        func dataScanner(
            _ dataScanner: DataScannerViewController,
            didAdd addedItems: [RecognizedItem],
            allItems: [RecognizedItem]
        ) {
            guard let first = addedItems.first,
                  case .barcode(let barcode) = first,
                  let payload = barcode.payloadStringValue else { return }
            // Haptic feedback on successful scan
            UINotificationFeedbackGenerator().notificationOccurred(.success)
            scannedCode = payload
        }

        func dataScanner(
            _ dataScanner: DataScannerViewController,
            becameUnavailableWithError error: DataScannerViewController.ScanningUnavailable
        ) {
            print("Scanner unavailable: \(error.localizedDescription)")
        }
    }
}

// MARK: - Content View

struct ContentView: View {
    @State private var model = ScannerViewModel()

    var body: some View {
        NavigationStack {
            VStack(spacing: 24) {
                if let code = model.scannedCode {
                    VStack(alignment: .leading, spacing: 8) {
                        Label("Last scanned", systemImage: "barcode.viewfinder")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                        Text(code)
                            .font(.title3.monospaced())
                            .padding()
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
                    }
                    .padding(.horizontal)
                    .transition(.move(edge: .top).combined(with: .opacity))
                } else {
                    ContentUnavailableView(
                        "No scan yet",
                        systemImage: "barcode",
                        description: Text("Tap the button below to scan a barcode or QR code.")
                    )
                }

                Button(action: model.presentScanner) {
                    Label("Scan Barcode", systemImage: "barcode.viewfinder")
                        .frame(maxWidth: .infinity)
                        .padding()
                }
                .buttonStyle(.borderedProminent)
                .controlSize(.large)
                .padding(.horizontal)
                .disabled(!model.isAvailable)

                if let error = model.errorMessage {
                    Text(error)
                        .font(.caption)
                        .foregroundStyle(.red)
                }
            }
            .navigationTitle("Barcode Scanner")
            .animation(.spring, value: model.scannedCode)
            .sheet(isPresented: $model.isScannerPresented) {
                BarcodeScannerView(
                    scannedCode: Binding(
                        get: { model.scannedCode },
                        set: { model.handleScan($0) }
                    ),
                    symbologies: [.qr, .ean13, .code128, .pdf417]
                )
                .ignoresSafeArea()
                .presentationDetents([.large])
            }
        }
    }
}

#Preview {
    ContentView()
}

How it works

  1. Availability guardDataScannerViewController.isSupported returns false on simulators and devices without a neural engine; isAvailable additionally reflects whether the user has granted camera permission. The view model checks both before presenting.
  2. Symbology filter — Passing .barcode(symbologies: [.qr, .ean13, ...]) to the recognizedDataTypes set restricts which code formats are detected, reducing false positives and improving speed.
  3. Coordinator delegate — The Coordinator adopts DataScannerViewControllerDelegate and implements didAdd(_:allItems:), which fires each time a new item enters the camera frame. Pattern matching on the .barcode case extracts the string payload safely.
  4. Haptic feedbackUINotificationFeedbackGenerator().notificationOccurred(.success) gives a tactile confirmation the moment a barcode is recognized, which is critical UX for scanning workflows.
  5. Sheet + state resethandleScan(_:) in the view model both stores the result and dismisses the scanner in one call, preventing the sheet from lingering open after a successful scan.

Variants

Continuous scanning (scan many codes without dismissing)

// Set recognizesMultipleItems: true and accumulate into an array

struct ContinuousBarcodeScannerView: UIViewControllerRepresentable {
    @Binding var scannedCodes: [String]

    func makeUIViewController(context: Context) -> DataScannerViewController {
        let scanner = DataScannerViewController(
            recognizedDataTypes: [.barcode()],
            recognizesMultipleItems: true,   // ← key change
            isHighlightingEnabled: true
        )
        scanner.delegate = context.coordinator
        try? scanner.startScanning()
        return scanner
    }

    func updateUIViewController(_ vc: DataScannerViewController, context: Context) {}
    func makeCoordinator() -> Coordinator { Coordinator(codes: $scannedCodes) }

    final class Coordinator: NSObject, DataScannerViewControllerDelegate {
        @Binding var codes: [String]
        init(codes: Binding<[String]>) { _codes = codes }

        func dataScanner(_ scanner: DataScannerViewController,
                         didAdd items: [RecognizedItem],
                         allItems: [RecognizedItem]) {
            let newPayloads = items.compactMap {
                if case .barcode(let b) = $0 { return b.payloadStringValue }
                return nil
            }
            codes.append(contentsOf: newPayloads)
        }
    }
}

Combining barcode + text recognition

Pass both .barcode() and .text(textContentType: nil) in the recognizedDataTypes set. In the delegate, switch over both cases: case .barcode(let b) and case .text(let t). This is useful for label-scanning flows where you need both a product code and a printed lot number from the same label. Note that mixing types may reduce barcode recognition speed on older A-series chips.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement a barcode scanner in SwiftUI for iOS 17+.
Use VisionKit/DataScannerViewController wrapped in UIViewControllerRepresentable.
Support QR, EAN-13, Code 128, and PDF417 symbologies.
Add availability checking with DataScannerViewController.isSupported.
Make it accessible (VoiceOver announcement on scan via UIAccessibility.post).
Add a #Preview with realistic sample data.

In Soarias, drop this prompt into the Build phase after your screens are scaffolded — the scanner view will be wired into your navigation stack and Info.plist camera permission updated automatically.

Related

FAQ

Does this work on iOS 16?

DataScannerViewController was introduced in iOS 16, so basic barcode scanning works there. However, the qualityLevel initializer parameter and several symbologies such as .microQR and .msiPlessey are iOS 17+ only. The code in this guide targets iOS 17+ explicitly. If you need iOS 16 support, use a deployment guard and omit those parameters.

How do I restrict scanning to QR codes only?

Pass .barcode(symbologies: [.qr]) as the sole element in the recognizedDataTypes set. This tells the Vision framework to ignore all other barcode formats, which speeds up recognition and prevents accidental reads of irrelevant codes nearby. You can mix any values from VNBarcodeSymbology.ean8, .upce, .aztec, etc.

What's the UIKit equivalent?

Before VisionKit's DataScannerViewController, the standard approach was AVCaptureSession with an AVCaptureMetadataOutput, which required manually configuring the capture session, preview layer, and output delegate. That approach still works but involves roughly 3× the boilerplate. For any new app targeting iOS 16+, DataScannerViewController is the recommended replacement.

Last reviewed: 2026-05-12 by the Soarias team.

```