DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

Measuring Barcode Scanning Performance: Distance vs Module Size with iOS ARKit

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

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:

  1. ARKit - Apple's augmented reality framework for accurate distance measurement
  2. Dynamsoft Barcode Reader - A commercial SDK we'll evaluate (you can substitute any SDK)
  3. 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)
Enter fullscreen mode Exit fullscreen mode

The app uses SwiftUI's ZStack to layer:

  1. Bottom layer: AR camera view with barcode detection
  2. 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
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • ARWorldTrackingConfiguration enables 6DOF tracking and distance measurement
  • sceneDepth activates 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 

                ...
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • ARKit provides YUV (NV12) pixel buffers, not RGB
  • Must handle device orientation for correct image interpretation
  • moduleSize is 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
    )
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Try multiple hit test strategies: feature points (LiDAR/visual), existing planes, estimated planes
  • simd_distance computes 3D Euclidean distance between world positions
  • Filter out unrealistic values (too close or too far)
  • Returns nil if 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)
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

barcode scanning with distance and module size overlay

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

  1. Print test barcodes of various physical sizes:

    • Small: 2cm × 2cm QR codes
    • Medium: 5cm × 5cm QR codes
    • Large: 10cm × 10cm QR codes
  2. 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!
  3. 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

Source Code

https://github.com/yushulx/ios-swiftui-barcode-mrz-document-scanner/tree/main/examples/BarcodeDistance

Top comments (0)