●TEST — The Rork Companion app lets you test on a real iPhone without a paid Apple Developer account●CLOUD — Code compiles on a cloud Mac, streaming a 60fps live simulator with real touch input●BROWSER — Design, code, and test entirely in Chrome or Safari — no Xcode required●PUBLISH — Two-click App Store publishing keeps the submission process simple●MAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, and Vision Pro●RN — Standard Rork generates iOS and Android apps together with React Native (Expo)●TEST — The Rork Companion app lets you test on a real iPhone without a paid Apple Developer account●CLOUD — Code compiles on a cloud Mac, streaming a 60fps live simulator with real touch input●BROWSER — Design, code, and test entirely in Chrome or Safari — no Xcode required●PUBLISH — Two-click App Store publishing keeps the submission process simple●MAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, and Vision Pro●RN — Standard Rork generates iOS and Android apps together with React Native (Expo)
Building a Live Barcode and Text Scanner in Rork Max with VisionKit's DataScanner
Add a live barcode and text scanner to your Rork Max native Swift app using VisionKit's DataScanner. Covers the SwiftUI bridge, availability handling, throttling repeated detections, and on-device verification with working code.
When I asked Rork Max to extend a generated native Swift screen with "let me scan a product barcode," the browser live simulator showed an empty frame and the camera never opened. After some digging the answer was simple: VisionKit's DataScannerViewController does not run in the simulator at all. It only starts on a supported physical device. Rork has been smoothing out real-device testing through its Companion app and cloud Mac compilation in 2026, and camera-dependent features like this are exactly the kind you should plan to verify on hardware from the very start.
This walkthrough adds a screen that recognizes barcodes and text live, at the same time, to your Rork Max native Swift code — including the SwiftUI bridge and on-device verification. As an indie developer who has shipped apps to the App Store since 2014, I have learned that camera features trip you up most often on two fronts: permissions and device differences. I will call those out as we go.
Why pick DataScanner over hand-rolled AVFoundation
There are two common ways to read codes from the camera on iOS. The classic route is wiring up AVCaptureSession yourself and pulling barcodes from AVCaptureMetadataOutput. The other is VisionKit's DataScannerViewController, introduced in iOS 16, which bundles the camera preview, subject highlighting, pinch-to-zoom, and guidance UI together.
Aspect
Hand-rolled AVFoundation
VisionKit DataScanner
Preview rendering
Place AVCaptureVideoPreviewLayer yourself
Built in
Text recognition
Combine with Vision separately
Same API as barcodes, captured together
Highlighting
Draw rectangles manually
Standard via isHighlightingEnabled
Device support
Works broadly with any camera
A12 Bionic and later (Neural Engine) only
Simulator
Sometimes works, limited
Unsupported (real device required)
For cases where you want to read a product code and a model-number string together, DataScanner is dramatically shorter to write. The trade-off is the limited device support and the simulator gap, both of which you need to design around from the beginning. Before I write a line of scanner code, I decide what to show when scanning is not available.
Step 1: Declare the permission and request camera access
First, add NSCameraUsageDescription to Info.plist. You can edit it from the Rork Max project settings too. A specific, purpose-driven string removes one common App Store review rejection reason.
<key>NSCameraUsageDescription</key><string>Used to scan product barcodes and model numbers with the camera.</string>
Request access explicitly before showing the screen. DataScannerViewController.isAvailable returns false when the camera is not authorized, so if you check availability without first prompting, you will misclassify a perfectly capable device as "unsupported." That was my first stumble.
import AVFoundationfunc requestCameraAccess() async -> Bool { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: return true case .notDetermined: // The dialog appears here on first run. Call this BEFORE checking availability. return await AVCaptureDevice.requestAccess(for: .video) case .denied, .restricted: return false @unknown default: return false }}
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Turn a scanner feature that silently failed in the simulator into one that reliably runs on real devices, with a proper availability gate
✦Get a UIViewControllerRepresentable wrapper that bridges DataScannerViewController into SwiftUI, plus a 1.5-second throttle that stops the same code from firing over and over
✦Assemble a screen that recognizes barcodes and text at the same time, from the permission prompt to the result display
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
DataScannerViewController exposes both isSupported (does the hardware and OS support it) and isAvailable (supported, authorized, and currently usable). Holding these as a state you can branch on — rather than a plain boolean — makes on-device behavior far easier to reason about.
import VisionKitenum ScannerAvailability { case ready case cameraDenied // not authorized -> guide to Settings case unsupportedDevice // simulator / A11 or earlier case restricted // parental controls, etc. static func current() -> ScannerAvailability { // isSupported only reflects device/OS capability, not authorization guard DataScannerViewController.isSupported else { return .unsupportedDevice } // isAvailable is true only when supported AND authorized AND usable guard DataScannerViewController.isAvailable else { let status = AVCaptureDevice.authorizationStatus(for: .video) return status == .restricted ? .restricted : .cameraDenied } return .ready }}
Step 3: Bridge DataScanner into SwiftUI
DataScannerViewController is a UIKit view controller, so wrap it with UIViewControllerRepresentable to use it from SwiftUI. Pass detections out through a closure, and keep the throttle that suppresses repeated detections in the coordinator. Without it, the same barcode triggers didAdd continuously while it stays in view, making the result flicker or hammering your server with repeat lookups.
import SwiftUIimport VisionKitstruct DataScannerView: UIViewControllerRepresentable { let recognizedTypes: Set<DataScannerViewController.RecognizedDataType> @Binding var isScanning: Bool let onScan: (RecognizedItem) -> Void func makeUIViewController(context: Context) -> DataScannerViewController { let controller = DataScannerViewController( recognizedDataTypes: recognizedTypes, qualityLevel: .balanced, recognizesMultipleItems: false, isHighFrameRateTrackingEnabled: true, isPinchToZoomEnabled: true, isGuidanceEnabled: true, isHighlightingEnabled: true ) controller.delegate = context.coordinator return controller } func updateUIViewController(_ controller: DataScannerViewController, context: Context) { // Start or stop scanning to match the parent state if isScanning { try? controller.startScanning() } else { controller.stopScanning() } } func makeCoordinator() -> Coordinator { Coordinator(self) } final class Coordinator: NSObject, DataScannerViewControllerDelegate { private let parent: DataScannerView private var lastFired: [String: Date] = [:] init(_ parent: DataScannerView) { self.parent = parent } func dataScanner(_ scanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) { for item in addedItems { handle(item) } } func dataScanner(_ scanner: DataScannerViewController, didTapOn item: RecognizedItem) { handle(item) } // Do not re-notify the same content for 1.5 seconds private func handle(_ item: RecognizedItem) { let key = stableKey(for: item) let now = Date() if let last = lastFired[key], now.timeIntervalSince(last) < 1.5 { return } lastFired[key] = now parent.onScan(item) } private func stableKey(for item: RecognizedItem) -> String { switch item { case .barcode(let code): return "barcode:" + (code.payloadStringValue ?? "") case .text(let text): return "text:" + text.transcript @unknown default: return "unknown" } } }}
Declaring what to recognize
recognizedDataTypes declares what to read. Narrowing barcode symbologies cuts false positives, and specifying languages improves text accuracy.
Switch between the scanner, a permission prompt, and an unsupported notice. On unsupported devices and the simulator, spell out "verify on a real device" with ContentUnavailableView — it helps both you during testing and your users.
struct ScannerScreen: View { @State private var availability: ScannerAvailability = .unsupportedDevice @State private var isScanning = true @State private var lastResult = "" private let types: Set<DataScannerViewController.RecognizedDataType> = [ .barcode(symbologies: [.qr, .ean13, .code128]), .text(languages: ["en", "ja"]) ] var body: some View { Group { switch availability { case .ready: DataScannerView(recognizedTypes: types, isScanning: $isScanning) { item in switch item { case .barcode(let code): lastResult = code.payloadStringValue ?? "(empty code)" case .text(let text): lastResult = text.transcript @unknown default: break } } .overlay(alignment: .bottom) { Text(lastResult.isEmpty ? "Point at a code" : lastResult) .padding() .background(.thinMaterial, in: .capsule) .padding(.bottom, 32) } case .cameraDenied: ContentUnavailableView( "Camera access needed", systemImage: "camera.fill", description: Text("Allow camera use from the Settings app.") ) case .unsupportedDevice: ContentUnavailableView( "Not available on this device", systemImage: "iphone.slash", description: Text("The simulator and A11-or-earlier devices do not support DataScanner. Verify on a real device.") ) case .restricted: ContentUnavailableView( "Camera is restricted", systemImage: "lock.fill", description: Text("Check Screen Time and other restrictions.") ) } } .task { _ = await requestCameraAccess() availability = ScannerAvailability.current() } }}
What actually bit me during on-device testing
Here are the operational notes that are easy to miss if you only read the official docs.
It never starts in the simulator.isSupported returns false, so trying to verify in the simulator leaves you stuck on "unsupported" forever. Getting onto hardware early through Rork's Companion app or cloud compilation, and testing camera code on a real device from the start, is in my experience the fastest path overall.
Do not check availability before prompting.isAvailable is false when unauthorized. Run requestAccess first, then evaluate — otherwise you reject a device that would work once permission is granted.
Throttling is mandatory.didAdd fires repeatedly while the subject is in view. In production, simply blocking the same payload for a short window stabilizes the feel enormously. The 1.5-second value is what settled out for me across test builds; tune it between 1.0 and 2.0 seconds for your app.
Power and thermals. High frame-rate tracking generates heat. On screens that scan for a long time, I recommend calling stopScanning() once a result is confirmed to pause the session.
One step after the read
A scan is not finished at "it read something." Barcode payloads can carry leading or trailing whitespace and control characters, so trim them with trimmingCharacters(in: .whitespacesAndNewlines) before any server lookup. Text recognition (OCR) introduces misreads, so for fixed-format values like model numbers, run a light regex validation first to keep downstream logic stable.
The SwiftUI that Rork Max generates has a straightforward structure, so native camera features like this become production-ready once you add two safeguards to the generated skeleton: an availability gate and throttling. Start by getting one supported device in hand and confirming that the ContentUnavailableView never appears.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.