When evaluating barcode scanning SDKs, you'll often encounter bold marketing claims: "Scans barcodes from 5 meters away!" or "Industry-leading long-distance scanning!" But here's the truth that vendors don't want you to know: distance alone is a meaningless metric.
In this article, I'll show you how to build an iOS app using ARKit and Dynamsoft Barcode Reader SDK to measure the real performance indicator: module size. You'll learn why a barcode SDK that claims to scan from 10 meters away might perform worse than one that "only" scans from 2 meters, and how to objectively evaluate barcode scanning performance.
Demo Video: Display Barcode Module Size and Distance in Real-Time
The video below shows a demo app that runs on an iPhone 11, using ARKit to measure distance and Dynamsoft Barcode Reader to detect barcodes. The overlay displays both the distance to the barcode and its module size in pixels.
Prerequisites
- Trial license for Dynamsoft Barcode Reader
The Distance Myth: Why Marketing Claims Are Misleading
The Problem with Distance-Based Claims
Many companies market their barcode SDKs with impressive distance claims:
- "Read barcodes up to 10 meters away!"
- "5x farther than competitors!"
- "Long-distance scanning capability!"
Here's why these claims are deceptive:
If a barcode is physically large, any decent SDK can scan it from far away. The real question isn't "how far?" but rather "how small of a module size can your SDK reliably decode?"
What Is Module Size?
Module size is the width (in pixels) of the smallest element in a barcode:
- For QR codes: the size of one black or white square
- For 1D barcodes: the width of the narrowest bar
This is the metric that truly determines scanning difficulty:
- Large module size (e.g., 20+ pixels): Easy to scan, even from distance
- Small module size (e.g., 5-10 pixels): Challenging, requires good algorithm
- Very small module size (< 5 pixels): Often at the limit of SDK capability
The Real Relationship: Distance + Physical Size = Module Size
Consider these scenarios:
| Physical Barcode Size | Distance | Module Size in Camera | Difficulty |
|---|---|---|---|
| 10cm × 10cm QR code | 3 meters | 15 pixels | Easy |
| 2cm × 2cm QR code | 50cm | 15 pixels | Easy (same!) |
| 10cm × 10cm QR code | 8 meters | 5 pixels | Hard |
| 1cm × 1cm QR code | 40cm | 5 pixels | Hard (same!) |
The scanning difficulty is identical when module size is the same, regardless of the actual physical distance. A vendor claiming "10-meter scanning" might just be testing with poster-sized barcodes!
Building a Real Performance Measurement Tool
To objectively evaluate barcode SDK performance, we need to measure module size alongside distance. This iOS app demonstrates how to do exactly that using:
- ARKit - Apple's augmented reality framework for accurate distance measurement
- Dynamsoft Barcode Reader - A commercial SDK we'll evaluate (you can substitute any SDK)
- Real-time overlay - Display both distance AND module size simultaneously
Demo App Features
- Real-time barcode detection with Dynamsoft iOS Barcode Reader SDK
- Accurate distance measurement using ARKit hit testing and LiDAR (when available)
- Module size reporting directly from SDK
- Visual overlay showing all metrics simultaneously
- Support for all major barcode formats (QR, Code 128, EAN, etc.)
Architecture Overview
┌─────────────────────────────────────┐
│ ContentView.swift │
│ (Main UI with ZStack layout) │
└──────────┬──────────────────────────┘
│
├─► ARBarcodeScanner.swift
│ ├─ ARSCNView (Camera + AR session)
│ ├─ ARKit distance calculation
│ └─ Dynamsoft barcode detection
│
└─► BarcodeOverlayView.swift
└─ Visual annotations (distance + module size)
The app uses SwiftUI's ZStack to layer:
- Bottom layer: AR camera view with barcode detection
- Top layer: Overlay annotations showing results
Step-by-Step Code Walkthrough
Step 1: Data Model - BarcodeDetection Structure
First, we define a data structure to hold all the information we need about each detected barcode:
struct BarcodeDetection: Identifiable {
let id = UUID()
let value: String // Barcode content
let type: String // Format (QR, Code128, etc.)
let distance: Float // Distance from camera (meters)
let position: CGPoint // Screen position for overlay
let bounds: CGRect // Bounding box for drawing
let moduleSize: Int // THE KEY METRIC: module size in pixels
}
The moduleSize property is what makes this tool valuable - it's the objective performance metric we're after.
Step 2: ARKit Setup and Session Management
We use ARSCNView to get camera access and AR tracking capabilities:
struct ARBarcodeScanner: UIViewRepresentable {
@Binding var detectedBarcodes: [BarcodeDetection]
func makeUIView(context: Context) -> ARSCNView {
let arView = ARSCNView()
arView.delegate = context.coordinator
arView.session.delegate = context.coordinator
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = []
configuration.environmentTexturing = .none
if ARWorldTrackingConfiguration.supportsFrameSemantics(.sceneDepth) {
configuration.frameSemantics = .sceneDepth
}
arView.session.run(configuration)
return arView
}
func makeCoordinator() -> ARBarcodeScannerCoordinator {
ARBarcodeScannerCoordinator(parent: self)
}
}
Key points:
-
ARWorldTrackingConfigurationenables 6DOF tracking and distance measurement -
sceneDepthactivates LiDAR sensor for precise depth data (if hardware supports it) - We disable plane detection and environment texturing to conserve battery/performance
Step 3: Frame Processing and Throttling
Processing every single camera frame would drain battery and CPU. We implement throttling:
func session(_ session: ARSession, didUpdate frame: ARFrame) {
let currentTime = Date().timeIntervalSince1970
guard !isProcessing && (currentTime - lastProcessTime) >= processingInterval else { return }
isProcessing = true
lastProcessTime = currentTime
let currentOrientation: UIInterfaceOrientation
if Thread.isMainThread {
currentOrientation = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first?.interfaceOrientation ?? .portrait
} else {
currentOrientation = .portrait
}
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.detectBarcodes(in: frame, orientation: currentOrientation)
self?.isProcessing = false
}
}
Key points:
- Process every 100ms (10 FPS) instead of 60 FPS - enough for smooth UX
- Capture UI orientation on main thread (UIKit requirement)
- Perform heavy work on background queue
- Use
[weak self]to prevent retain cycles
Step 4: Dynamsoft SDK Integration
Now the core barcode detection using Dynamsoft Barcode Reader SDK:
private func detectBarcodes(in frame: ARFrame, orientation: UIInterfaceOrientation) {
let pixelBuffer = frame.capturedImage
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else { return }
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let bufferSize = CVPixelBufferGetDataSize(pixelBuffer)
let buffer = Data(bytes: baseAddress, count: bufferSize)
let imageData = ImageData(
bytes: buffer,
width: UInt(width),
height: UInt(height),
stride: UInt(width),
format: .NV12,
orientation: getDynamsoftOrientation(for: orientation),
tag: nil
)
let result = cvr.captureFromBuffer(imageData, templateName: PresetTemplate.readBarcodes.rawValue)
if let items = result.items {
for item in items {
if item.type == .barcode, let barcodeItem = item as? BarcodeResultItem {
let format = barcodeItem.formatString
let text = barcodeItem.text
let points = barcodeItem.location.points
let moduleSize = barcodeItem.moduleSize
...
}
}
}
}
Key points:
- ARKit provides YUV (NV12) pixel buffers, not RGB
- Must handle device orientation for correct image interpretation
-
moduleSizeis provided directly by Dynamsoft SDK - this is the gold standard metric - Always unlock pixel buffers when done
Step 5: Coordinate Mapping - The Challenging Part
Camera resolution (e.g., 1920×1080) differs from screen size (e.g., 414×896 points). We must map barcode coordinates correctly:
let viewInfo = getARViewSize()
let arViewPointSize = viewInfo.pointSize
let arViewPixelSize = viewInfo.pixelSize
let screenScale = viewInfo.scale
let (cameraWidth, cameraHeight) = getOrientedCameraDimensions(
bufferWidth: width,
bufferHeight: height,
orientation: orientation
)
let scaleX = arViewPixelSize.width / CGFloat(cameraWidth)
let scaleY = arViewPixelSize.height / CGFloat(cameraHeight)
let scale = max(scaleX, scaleY)
let scaledImageWidth = CGFloat(cameraWidth) * scale
let scaledImageHeight = CGFloat(cameraHeight) * scale
let cropOffsetX = (scaledImageWidth - arViewPixelSize.width) / 2
let cropOffsetY = (scaledImageHeight - arViewPixelSize.height) / 2
let scaledPixelPoints = points.map { point in
CGPoint(
x: point.x * scale - cropOffsetX,
y: point.y * scale - cropOffsetY
)
}
let scaledPoints = scaledPixelPoints.map { pixelPoint in
CGPoint(
x: pixelPoint.x / screenScale,
y: pixelPoint.y / screenScale
)
}
Key challenges:
- Points vs Pixels: iOS uses "points" for UI, but camera uses pixels. On retina displays (2× or 3×), 1 point = multiple pixels
- Aspect Fill: Camera feed is cropped to fill screen
- Orientation: Camera buffer is fixed landscape orientation
Step 6: Distance Calculation with ARKit
ARKit provides hit testing to determine 3D world coordinates:
private func calculateDistance(at point: CGPoint, frame: ARFrame) -> Float? {
var results = frame.hitTest(point, types: .featurePoint)
if results.isEmpty {
results = frame.hitTest(point, types: .existingPlane)
}
if results.isEmpty {
results = frame.hitTest(point, types: .estimatedHorizontalPlane)
}
guard let result = results.first else { return nil }
let distance = simd_distance(
result.worldTransform.columns.3,
frame.camera.transform.columns.3
)
guard distance > 0.1 && distance < 10.0 else { return nil }
return distance
}
Key points:
- Try multiple hit test strategies: feature points (LiDAR/visual), existing planes, estimated planes
-
simd_distancecomputes 3D Euclidean distance between world positions - Filter out unrealistic values (too close or too far)
- Returns
nilif no reliable depth information available
On LiDAR devices (iPhone 12 Pro and newer Pro models), this is extremely accurate (±1cm). On non-LiDAR devices, it uses visual SLAM which is less precise but still usable.
Step 7: UI Overlay with Both Metrics
Finally, we display both distance AND module size together:
struct BarcodeAnnotationView: View {
let barcode: BarcodeDetection
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(barcode.type)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.8))
.foregroundColor(.white)
.cornerRadius(4)
Text(barcode.value)
.font(.system(.body, design: .monospaced))
.padding(.horizontal, 8)
.padding(.vertical, 6)
.background(Color.black.opacity(0.7))
.foregroundColor(.white)
.cornerRadius(6)
HStack(spacing: 8) {
HStack(spacing: 4) {
Image(systemName: "ruler")
Text(formatDistance(barcode.distance))
.fontWeight(.bold)
}
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.green.opacity(0.8))
.foregroundColor(.white)
.cornerRadius(4)
HStack(spacing: 4) {
Image(systemName: "square.grid.3x3")
Text("\(barcode.moduleSize)px")
.fontWeight(.bold)
}
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.orange.opacity(0.8))
.foregroundColor(.white)
.cornerRadius(4)
}
}
.position(x: barcode.position.x, y: barcode.position.y)
.overlay(
Rectangle()
.stroke(Color.green, lineWidth: 2)
.frame(width: barcode.bounds.width, height: barcode.bounds.height)
.position(x: barcode.bounds.midX, y: barcode.bounds.midY)
)
}
}
How to Use This Tool for SDK Evaluation
Now that you have the app running, here's how to objectively test barcode SDK performance:
Test Procedure
-
Print test barcodes of various physical sizes:
- Small: 2cm × 2cm QR codes
- Medium: 5cm × 5cm QR codes
- Large: 10cm × 10cm QR codes
-
For each barcode size, gradually move away until detection fails:
- Record the module size at the maximum working distance
- The distance itself doesn't matter!
-
Compare SDKs by the minimum module size they can reliably decode:
- SDK A: Works down to 8px module size
- SDK B: Works down to 5px module size
- SDK B is objectively better, regardless of physical distance
What Good Module Size Performance Looks Like
| Module Size Range | Performance Level | Notes |
|---|---|---|
| 20+ pixels | Excellent | Should be 100% reliable for any decent SDK |
| 10-20 pixels | Good | Normal scanning conditions |
| 5-10 pixels | Challenging | Separates good SDKs from mediocre ones |
| 3-5 pixels | Difficult | Premium SDKs only; may require good lighting |
| < 3 pixels | Extreme | At or beyond most SDK capabilities |

Top comments (0)