How to implement a barcode scanner in SwiftUI
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
-
Availability guard —
DataScannerViewController.isSupportedreturnsfalseon simulators and devices without a neural engine;isAvailableadditionally reflects whether the user has granted camera permission. The view model checks both before presenting. -
Symbology filter — Passing
.barcode(symbologies: [.qr, .ean13, ...])to therecognizedDataTypesset restricts which code formats are detected, reducing false positives and improving speed. -
Coordinator delegate — The
CoordinatoradoptsDataScannerViewControllerDelegateand implementsdidAdd(_:allItems:), which fires each time a new item enters the camera frame. Pattern matching on the.barcodecase extracts the string payload safely. -
Haptic feedback —
UINotificationFeedbackGenerator().notificationOccurred(.success)gives a tactile confirmation the moment a barcode is recognized, which is critical UX for scanning workflows. -
Sheet + state reset —
handleScan(_:)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
-
iOS 16 crash:
DataScannerViewControllerwas introduced in iOS 16, but thequalityLeveland some symbologies (e.g..microQR) require iOS 17+. Always wrap initialization in theisSupportedcheck and set your deployment target accordingly. -
Missing NSCameraUsageDescription: Forgetting to add the
NSCameraUsageDescriptionkey toInfo.plistcauses a silent crash at launch on device. The description string is displayed in the system permission dialog — make it meaningful (e.g. "Used to scan product barcodes"). -
Memory leak via Coordinator: The
DataScannerViewControllerholds a strong reference to its delegate. SinceCoordinatoris anNSObjectsubclass you create, avoid capturingselfstrongly inside closures within it to prevent a retain cycle. -
VoiceOver accessibility:
DataScannerViewControllerdoes not announce scanned results to VoiceOver automatically. Post aUIAccessibility.post(notification: .announcement, argument: payload)insidedidAddso assistive technology users hear the result.
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.