DLens SDK Documentation

DLens is a production-grade iOS SDK for scanning driver licences, passports, and national ID cards — entirely on-device, with no data stored or transmitted.

v1.8.0 · Stable iOS 17.0+ Swift 5.9+ Xcode 15+

Overview

DLens uses the iOS Vision framework and AVFoundation to detect and parse identity documents directly on device. No network connection is required. No document data leaves the device at any point.

Every frame is first passed through document detection (Vision document segmentation with perspective correction), so the decoders below work on a flattened, tightly cropped, upright image of the card rather than the whole frame. MRZ reads are then voted character-by-character across frames and validated by the ICAO checksums before a result is returned.

Beneath that, the SDK runs four decoding pipelines:

Requirements

The framework is fully self-contained — it links only Apple system frameworks plus a statically bundled copy of zxing-cpp (Apache License 2.0). There is nothing extra to embed, and no other third-party dependencies.

Installation

XCFramework (Recommended)

1

Download the framework

Request your trial key at /dlens/#contact — we'll send you DLensSDK.xcframework along with your key.

2

Add to your project

Drag DLensSDK.xcframework into your Xcode project navigator. In the dialog, check Copy items if needed.

3

Embed & Sign

In your target's General settings, open Frameworks, Libraries, and Embedded Content, then set DLensSDK.xcframework to Embed & Sign.

4

Add camera permission

Add NSCameraUsageDescription to your Info.plist with a description explaining why your app needs camera access.

Tip: The framework must be Embed & Sign, not just Do Not Embed. Without embedding, the framework won't be copied into your app bundle and the install will fail with a missing signature error.

Quick Start

Three steps to your first scan:

DLensSampleApp.swift
import SwiftUI
import DLensSDK

// Step 1: Initialise at app launch
@main
struct MyApp: App {
    init() {
        try? DLensKit.initialize(
            apiKey: "YOUR_API_KEY",
            secret: "YOUR_SECRET"
        )
    }
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

// Step 2: Present the scanner
struct ContentView: View {
    @State private var isScanning = false

    var body: some View {
        Button("Scan Licence") { isScanning = true }
        .fullScreenCover(isPresented: $isScanning) {

            // Step 3: Handle the result
            DLensScanner { result in
                print(result.fullName)
                print(result.licenseNumber)
                isScanning = false
            }
        }
    }
}

Initialisation

Call DLensKit.initialize() once at app launch — typically in your App.init() or AppDelegate.application(_:didFinishLaunchingWithOptions:). Calling it more than once is safe and is a no-op after the first successful call.

Initialisation
import DLensSDK

do {
    try DLensKit.initialize(
        apiKey: "YOUR_API_KEY",
        secret: "YOUR_SECRET",
        configuration: {
            var c = DLensConfiguration()
            c.showTorchButton   = true
            c.showZoomControls  = true
            c.scanMode          = .all
            return c
        }()
    )
} catch {
    // DLensError.invalidAPIKey — malformed, or locked to another bundle ID
    // DLensError.licenseKeyMismatch — key and secret do not match
    // DLensError.licenseExpired — the licence has passed its expiry date
    print("DLens init failed: \(error.localizedDescription)")
}

SwiftUI Integration

Use DLensScanner inside a .fullScreenCover or .sheet. It fills the presented area with the camera UI automatically.

SwiftUI
DLensScanner(
    onResult: { result in
        // Called on main thread when scan succeeds
        self.scanResult = result
        self.isScanning  = false
    },
    onError: { error in
        // Camera permission denied, key expired, etc.
        print(error.localizedDescription)
        self.isScanning = false
    },
    onCancel: {
        // User tapped the cancel/back button
        self.isScanning = false
    }
)

UIKit Integration

UIKit
let scanner = DLensScannerViewController(
    onResult: { [weak self] result in
        self?.handleResult(result)
    },
    onError: { error in
        print(error)
    }
)
present(scanner, animated: true)

Objective-C Integration

DLens ships a full Objective-C surface: DLNScanner, DLNScanResult, DLNScanMode, and DLNConfiguration. Swift apps should keep using DLensKit and DLensScanner directly.

Objective-C
// At launch
NSError *error = nil;
[DLNScanner initializeWithApiKey:apiKey secret:secret error:&error];

// Present the scanner
UIViewController *scanner = [DLNScanner
    makeScannerViewControllerWithMode:DLNScanModeAll
    onResult:^(DLNScanResult *r) { NSLog(@"%@", r.fullName); }
     onError:^(NSError *e) { }
    onCancel:^{ }];
[self presentViewController:scanner animated:YES completion:nil];

DLNConfiguration mirrors every property of the Swift DLensConfiguration:

Objective-C — configuration
DLNConfiguration *config = [DLNConfiguration new];
config.scanMode = DLNScanModeAll;
config.maxZoom = 3.0;
config.showTipCards = NO;

[DLNScanner initializeWithApiKey:apiKey secret:secret
                 configuration:config error:&error];

ℹ️ DLNScanner.sdkVersion reports the SDK version — NSObject already owns the +version selector, so it is named differently in Objective-C than in Swift.

Headless Scanning

Get parsed results without DLens presenting any camera UI — when you run your own capture session, already hold a photo, or have decoded the barcode yourself and only want the parsing.

Headless — Swift
// From an image you already hold — no DLens UI is presented
let result = try await DLensKit.scan(image: photo, mode: .all)
print(result.fullName)

// Straight from your own AVCaptureVideoDataOutput
let result = try await DLensKit.scan(pixelBuffer: buffer)

// From a payload you decoded yourself (AAMVA string or MRZ lines)
let result = try DLensKit.parse(payload: rawString)
MethodReturnsNotes
scan(image:mode:)async throwsUIImage — normalised upright and size-capped before analysis
scan(cgImage:mode:)async throwsCGImage variant
scan(pixelBuffer:mode:)async throwsCVPixelBuffer — for your own capture pipeline
parse(payload:)throwsNo image processing; returns synchronously
Headless — Objective-C
[DLNScanner scanImage:photo mode:DLNScanModeAll
           completion:^(DLNScanResult *r, NSError *e) {
    if (r) NSLog(@"%@", r.fullName);
}];

ℹ️ The headless path shares one implementation with the live scanner, so a document analysed either way produces an identical DLensScanResult. Both throw DLensError.scanningFailed when no document is found — they never return a blank result.

Configuration

PropertyTypeDefaultDescription
scanModeDLensScanMode.barcodeOnlyMode used when a scanner is created without an explicit mode
timeoutSecondsTimeInterval60Seconds before the scan fails with DLensError.timeout. 0 disables the timeout
accentColorColor.blueTint for the progress and processing UI
orientationDLensOrientationMode.automaticHow the preview and analysed frames are oriented — see Orientation
autoZoomBooltrueAutomatic zoom sweep while scanning. When off, the scanner holds the last successful zoom
minZoomCGFloat1.0Lower bound for the sweep and the manual buttons
maxZoomCGFloat2.5Upper bound, capped at 8.0 and by the device maximum
rescanZoomOffsetCGFloat0.5How far below the last successful zoom the scanner opens, so the sweep glides up through the known-good level

UI visibility

Every built-in control can be hidden independently — useful when you want the camera and your own chrome.

PropertyTypeDefaultHides
showTorchButtonBooltrueTorch toggle (top-right)
showZoomControlsBooltrueManual −/+ zoom pill
showCancelButtonBooltrueCancel button
showTipCardsBooltrueThe three hint cards along the bottom
showInstructionCardBooltrueCentral live-feedback card
showModeBadgeBooltrue"Auto-Detect" badge (top-left)

⚠️ With showCancelButton = false and timeoutSeconds = 0 the scanner has no built-in exit. Dismiss it yourself, or keep a timeout.

Orientation

The scanner follows the host app's interface orientation by default, can track the physical device instead, or can be pinned. Both the preview and the frames handed to OCR rotate, so MRZ reading works sideways.

ModeBehaviour
.automaticMatches the host interface orientation. A portrait-only app behaves exactly like .fixed(.portrait). Default.
.followDeviceKeeps the horizon level by following the physical device, even when the interface is locked — kiosks, portrait-locked apps
.fixed(_).portrait, .portraitUpsideDown, .landscapeLeft, .landscapeRight — names mirror UIInterfaceOrientation
Orientation
// At initialisation
config.orientation = .followDevice

// While a scanner is on screen — applies immediately
DLensKit.setOrientation(.fixed(.landscapeLeft))
let current = DLensKit.currentOrientation   // DLensOrientation?, nil when no scanner is up

Objective-C: config.orientation = DLNOrientationModeFollowDevice;, [DLNScanner setOrientation:DLNOrientationModeLandscapeLeft];, DLNScanner.currentOrientation.

ℹ️ .automatic can only rotate into orientations your app declares in UISupportedInterfaceOrientations. If your app is portrait-only and you want a level preview when users turn the phone, use .followDevice.

Changing configuration at runtime

Pass a configuration to initialize for your defaults, then call DLensKit.configure(_:) whenever they change — useful for a settings screen. It applies to the next scanner you present; a scanner already on screen keeps the configuration it opened with. DLensKit.currentConfiguration reads back what is in effect.

Runtime configuration
var c = DLensKit.currentConfiguration
c.showTipCards = false
c.maxZoom = 4.0
try DLensKit.configure(c)   // affects the next scanner presented

Objective-C: [DLNScanner configure:config error:&error].

ℹ️ Both sample apps ship a Settings screen wired to every one of these options, so you can change them on-device and see the effect immediately.

DLensScanResult

All fields are optional String? or Date? unless marked otherwise.

FieldTypeDescription
firstNameStringGiven name — always present
lastNameStringSurname — always present
middleNameString?Middle name (AAMVA and MRZ)
suffixString?Name suffix — JR, SR, III… (AAMVA)
fullNameStringComputed: firstName + middleName + lastName
dateOfBirthDate?Parsed date of birth
formattedDateOfBirthString?DOB formatted as "MMM d, yyyy"
licenseNumberStringDocument/licence number
licenseClassString?Vehicle class codes (AAMVA)
restrictionsString?Driving restrictions (AAMVA)
endorsementsString?Driving endorsements (AAMVA)
expiryDateDate?Document expiry date
formattedExpiryDateString?Expiry formatted as "MMM d, yyyy"
issueDateDate?Document issue date
streetAddressString?Street address (AAMVA)
cityString?City (AAMVA)
stateString?State or province code, e.g. "FL", "ON" (AAMVA)
postalCodeString?5-digit US ZIP, or Canadian postal code as "A1A 1A1" (AAMVA)
countryString?"USA" or "Canada" for licences; the nationality code for MRZ documents
genderString?"Male", "Female", or "Not specified"
eyeColorString?Eye colour code (AAMVA)
heightString?Height as 5'8" or 170 cm (AAMVA)
rawPayloadStringRaw barcode/MRZ string

Errors

Error CaseDescription
DLensError.notInitializedDLensKit.initialize() was not called before scanning
DLensError.invalidAPIKeyThe API key is missing, malformed, or was issued for a different bundle ID
DLensError.invalidSecretThe secret is missing or empty
DLensError.licenseKeyMismatchThe API key and secret do not match
DLensError.licenseExpiredThe licence encoded in the key has passed its expiry date
DLensError.cameraPermissionDeniedThe user denied camera access. Direct them to Settings.
DLensError.timeoutThe scanner reached timeoutSeconds without a result
DLensError.scanningFailed(String)No readable document was found — most often from the headless scan and parse APIs

Licence Keys

DLens uses HMAC-SHA256 signed JSON tokens. Each key encodes your bundle ID, expiry date, and a signature that can be verified entirely on-device without a network call.

Keep your secret private. Never commit your API key or secret to a public repository. For production apps, load credentials from your app's secure configuration — not hardcoded in source.

For development, you can generate test credentials that work without a paid licence:

Development Keys
// Generate dev credentials (for testing only)
let (apiKey, secret) = DLensKit.makeTestCredentials()
try DLensKit.initialize(apiKey: apiKey, secret: secret)

Driver Licences

DLens parses AAMVA-standard PDF417 barcodes from all 50 US states, Washington DC, and all Canadian provinces and territories. Both legacy AAMVA 2000 and modern ANSI D20 formats are supported with automatic version detection.

Scan the back of the licence where the PDF417 barcode is located. The scanner automatically activates hardware barcode detection via AVCaptureMetadataOutput for maximum speed.

Passports

DLens reads the Machine-Readable Zone (MRZ) on the photo data page of ICAO TD3-format passports from 190+ countries. Full ICAO 9303 check digit validation is performed on all fields before returning a result.

Scan the bottom two lines of the photo page (the MRZ zone). The scanner uses Vision framework OCR with a two-strategy parser to handle real-world imperfections.

National ID Cards

TD1 (three-line, 30 chars) and TD2 (two-line, 36 chars) MRZ formats are supported, covering EU national ID cards, Middle Eastern national IDs, biometric residence permits, and many other document types.

Developer FAQ

The scanner is not finding my barcode. What should I try?

Ensure good lighting, and keep the barcode flat and unfolded. The scanner opens just below the zoom level that last succeeded and then glides smoothly between minZoom and maxZoom until it decodes — widen that range if your documents are read at an unusual distance. Worn or damaged barcodes fall through to software decoding (zxing-cpp, then a multi-pass Vision ladder), which takes slightly longer.

Can I customise the scanner UI appearance?

Yes. DLensConfiguration.accentColor sets the tint, and every built-in control can be hidden independently — showTorchButton, showZoomControls, showCancelButton, showTipCards, showInstructionCard, and showModeBadge. If you want a completely custom camera experience, run your own capture session and use headless scanning instead.

How do I handle camera permission denial?

Catch DLensError.cameraPermissionDenied in your onError handler and redirect the user to UIApplication.openSettingsURLString.

Can I use DLens from Objective-C?

Yes — see Objective-C Integration. The bridge exposes DLNScanner, DLNScanResult, DLNScanMode, and DLNConfiguration, including the headless entry points.

Can I get results without showing the DLens camera?

Yes — headless scanning parses an image or a payload you already hold and presents no UI. Both paths share one implementation with the live scanner, so results are identical.

Does the SDK work offline?

Yes, completely. All scanning and parsing happen on-device, and the SDK makes no network requests at all — licence keys are HMAC-SHA256 tokens verified locally, with no activation call and nothing to re-validate. No image or scan result ever leaves the device.