Skip to main content

Driver's License Parsing

RequiresDL Parsing entitlement

The back of a US or Canadian driver's license carries a PDF417 barcode in the AAMVA format. This quickstart scans only PDF417, parses it with the SDK's AAMVAParser, and lays the result out as name, identification, address, demographics, dates, vehicle class, and compliance sections. It also checks whether DL parsing is licensed and tells the user if it is not.

Folder in the download: DLParsing/. Scheme: DLParsing.

What you'll see​

Driver's License Parsing app showing the camera preview and parsed license fields grouped in sections
Scan the PDF417 on the back of a license. Parsed fields appear grouped by section below the preview.

How it works​

Scan only PDF417. buildConfiguration() disables UPC, EAN, QR, and Code 128 and enables only .typePDF417, so the decoder does not spend time on other symbologies. config.dlParsingMode = .on tells the SDK to treat PDF417 results as license data when the entitlement is present.

Get the raw AAMVA payload. In the callback, PDF417 results may arrive as AilaDLScanObject, whose rawData holds the full barcode payload. The quickstart falls back to data if rawData is empty, and to the base AilaScanObject.data if the cast fails.

Parse it. AAMVAParser(data: raw).parse() returns a DriverLicense. license.isValid() confirms the payload parsed into something usable before it is published to the view. DriverLicense exposes typed properties such as firstName, licenseNumber, birthdate, height, expirationDate, and jurisdictionVehicleClass.

Check the entitlement. Aila_GetSDKLicensingInfo() returns a JSON string describing the current configuration and its features. The view model decodes it into SDKLicensingInfo and reads the parsing_dl flag for whichever platform is active, SoftScan or Kiosk. It rechecks whenever AilaHardwareChanged fires, since licensing can differ when a kiosk connects. When the flag is off, the view shows a banner and, after a scan, a "PDF417 scanned" message explaining that the barcode was received but not parsed.

DLParsing/ViewModel.swift
import Foundation
import Aila

private struct SDKLicensingInfo: Decodable {
let currentConfiguration: String?
let softScan: FeatureSet?
let kiosk: FeatureSet?

private enum CodingKeys: String, CodingKey {
case currentConfiguration
case softScan = "SoftScan"
case kiosk = "Kiosk"
}

struct FeatureSet: Decodable {
let parsingDl: ParsingDl?
enum CodingKeys: String, CodingKey { case parsingDl = "parsing_dl" }
struct ParsingDl: Decodable {
let enabled: Bool?
}
}

var isDLParsingEnabled: Bool {
let set = currentConfiguration == "SoftScan" ? softScan : kiosk
return set?.parsingDl?.enabled ?? false
}
}

final class DLParsingViewModel: ObservableObject {
@Published private(set) var parsedLicense: DriverLicense?
@Published private(set) var isDLParsingLicensed: Bool = true
@Published private(set) var receivedRawPDF417Only: Bool = false

var hasParsedLicense: Bool {
guard let license = parsedLicense else { return false }
return license.isValid()
}

private var hardwareChangedObserver: NSObjectProtocol?

func startScanning() {
Aila_Init()
Aila_SetConfiguration(buildConfiguration())
Aila_Start()

hardwareChangedObserver = NotificationCenter.default.addObserver(
forName: .AilaHardwareChanged,
object: nil,
queue: .main
) { [weak self] _ in
self?.updateDLParsingLicenseStatus()
}
}

private func updateDLParsingLicenseStatus() {
guard let jsonString = Aila_GetSDKLicensingInfo(),
let data = jsonString.data(using: .utf8) else {
isDLParsingLicensed = true
return
}
do {
let info = try JSONDecoder().decode(SDKLicensingInfo.self, from: data)
isDLParsingLicensed = info.isDLParsingEnabled
} catch {
isDLParsingLicensed = true
}
}

func stopScanning() {
if let observer = hardwareChangedObserver {
NotificationCenter.default.removeObserver(observer)
hardwareChangedObserver = nil
}
Aila_Stop()
}

func clearResult() {
parsedLicense = nil
receivedRawPDF417Only = false
}

private func buildConfiguration() -> AilaConfiguration {
let config = AilaConfiguration()
config.multiScanMode = .off
config.beepVolume = .volume3
config.dlParsingMode = .on

config.setCode(.typeUPC, enabled: false)
config.setCode(.typeEAN, enabled: false)
config.setCode(.typeQR, enabled: false)
config.setCode(.type128, enabled: false)
config.setCode(.typePDF417, enabled: true)

config.scanCallback = { [weak self] results in
self?.handleScanResults(results)
}

return config
}

private func handleScanResults(_ results: [AilaScanObject]?) {
guard let results = results, !results.isEmpty else { return }

for obj in results {
guard obj.type == .typePDF417 else { continue }

let rawData: String?
if let dlObj = obj as? AilaDLScanObject {
rawData = dlObj.rawData ?? dlObj.data
} else {
rawData = obj.data
}

guard let raw = rawData, !raw.isEmpty else { continue }

let parser = AAMVAParser(data: raw)
let license = parser.parse()
if license.isValid() {
DispatchQueue.main.async { [weak self] in
self?.parsedLicense = license
}
break
}
}
}
}

The view formats the DriverLicense for display. DLDisplayHelper handles dates, height in feet and inches, postal codes, and joining name and address parts.

DLParsing/View.swift
DLParsing/View.swift
import SwiftUI
import Aila

struct DLParsingView: View {
@StateObject private var viewModel = DLParsingViewModel()

var body: some View {
NavigationStack {
VStack(spacing: 0) {
if !viewModel.isDLParsingLicensed {
dlParsingUnlicensedBanner
}

Text("US Driver's License (PDF417)")
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(.vertical, 8)
AilaCaptureViewRepresentable()
.clipped()
.cornerRadius(16)
.padding(.horizontal)

if let license = viewModel.parsedLicense, viewModel.hasParsedLicense {
driverLicenseResultView(license)
} else if viewModel.receivedRawPDF417Only {
rawPDF417OnlyView
} else {
placeholderView
}
}
.padding(.bottom)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGroupedBackground))
.navigationTitle("Driver's License")
.toolbar {
if viewModel.hasParsedLicense || viewModel.receivedRawPDF417Only {
ToolbarItem(placement: .cancellationAction) {
Button("Clear") {
viewModel.clearResult()
}
}
}
}
.onAppear {
viewModel.startScanning()
}
.onDisappear {
viewModel.stopScanning()
}
}
}

private func driverLicenseResultView(_ license: DriverLicense) -> some View {
List {
Section("Name") {
if let fullName = DLDisplayHelper.fullName(from: license), !fullName.isEmpty {
row("Full Name", fullName)
}
}

Section("Identification") {
if let n = license.licenseNumber { row("License Number", n) }
if let n = license.documentId { row("Document ID", n) }
if let n = license.inventoryControlNumber { row("Inventory Control Number", n) }
}

Section("Address") {
if let addr = DLDisplayHelper.address(from: license), !addr.isEmpty {
row("Address", addr)
}
}

Section("Demographics") {
if let dob = license.birthdate { row("Date of Birth", DLDisplayHelper.formatDate(dob)) }
if let g = license.gender { row("Gender", String(describing: g)) }
if let e = license.eyeColor { row("Eye Color", String(describing: e)) }
if let h = license.height { row("Height", DLDisplayHelper.formatHeight(h)) }
if let pounds = license.weight.pounds { row("Weight", "\(Int(pounds)) lb") }
if let hc = license.hairColor { row("Hair Color", String(describing: hc)) }
}

Section("Dates") {
if let d = license.issueDate { row("Issue Date", DLDisplayHelper.formatDate(d)) }
if let d = license.expirationDate { row("Expiration Date", DLDisplayHelper.formatDate(d)) }
if let d = license.revisionDate { row("Revision Date", DLDisplayHelper.formatDate(d)) }
}

Section("Vehicle") {
if let c = license.jurisdictionVehicleClass { row("Vehicle Class", c) }
if let r = license.jurisdictionRestrictionCode, r.uppercased() != "NONE" { row("Restriction Code", r) }
if let e = license.jurisdictionEndorsementCode, e.uppercased() != "NONE" { row("Endorsement Code", e) }
}

Section("Compliance") {
if let c = license.complianceType { row("Compliance Type", String(describing: c)) }
if let v = license.version { row("Version", "\(v)") }
}
}
.listStyle(.insetGrouped)
.padding(.top, 8)
}

private func row(_ label: String, _ value: String) -> some View {
HStack(alignment: .top) {
Text(label)
.font(.subheadline)
.foregroundStyle(.secondary)
Spacer(minLength: 12)
Text(value)
.font(.body)
.multilineTextAlignment(.trailing)
}
}

private var placeholderView: some View {
VStack(spacing: 0) {
Text("Point camera at the PDF417 barcode on the back of a US driver's license")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
.padding(.top, 24)
Spacer(minLength: 0)
}
}

private var dlParsingUnlicensedBanner: some View {
Text("Driver's license parsing is not included in your current license.\nContact support@ailatech.com for licensing options.")
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 16)
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.background(Color(.secondarySystemBackground))
}

private var rawPDF417OnlyView: some View {
VStack(spacing: 12) {
Image(systemName: "doc.barcode")
.font(.largeTitle)
.foregroundStyle(.secondary)
Text("PDF417 scanned")
.font(.headline)
Text("Driver's license parsing is not available on this license, so the barcode was not parsed into license fields. Raw barcode data was received.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
Text("Contact support@ailatech.com for licensing options.")
.font(.caption)
.foregroundStyle(.secondary)
Button("Clear") {
viewModel.clearResult()
}
.padding(.top, 8)
Spacer(minLength: 0)
}
.padding(.top, 24)
}
}

private enum DLDisplayHelper {
static let dateFormatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
f.timeStyle = .none
return f
}()

static func formatDate(_ date: Date) -> String {
dateFormatter.string(from: date)
}

static func formatHeight(_ cm: Double) -> String {
let inches = cm / 2.54
let feet = Int(inches) / 12
let ins = Int(inches) % 12
return "\(feet)'\(ins)\""
}

static func formatPostalCode(_ postalCode: String) -> String {
let digits = postalCode.filter(\.isNumber)
if digits.count == 9 {
return "\(digits.prefix(5))-\(digits.suffix(4))"
}
return postalCode
}

static func displayCountry(_ country: IssuingCountry) -> String {
switch country {
case .unitedStates: return "United States"
case .canada: return "Canada"
@unknown default:
return "Unknown"
}
}

static func fullName(from license: DriverLicense) -> String? {
var parts: [String] = []
if let f = license.firstName { parts.append(f) }
if !license.middleNames.isEmpty { parts.append(license.middleNames.joined(separator: " ")) }
if let l = license.lastName { parts.append(l) }
if let s = license.suffix { parts.append(String(describing: s)) }
return parts.isEmpty ? nil : parts.joined(separator: " ")
}

static func address(from license: DriverLicense) -> String? {
var lines: [String] = []
if let s = license.streetAddress { lines.append(s) }
if let s = license.streetAddressTwo, s.uppercased() != "NONE" { lines.append(s) }
var cityStateZip: [String] = []
if let c = license.city { cityStateZip.append(c) }
if let s = license.state { cityStateZip.append(s) }
if let z = license.postalCode { cityStateZip.append(formatPostalCode(z)) }
if !cityStateZip.isEmpty { lines.append(cityStateZip.joined(separator: ", ")) }
if let c = license.country { lines.append(displayCountry(c)) }
return lines.isEmpty ? nil : lines.joined(separator: "\n")
}
}
DLParsing/AilaCaptureViewRepresentable.swift
DLParsing/AilaCaptureViewRepresentable.swift
import SwiftUI
import AVFoundation
import Aila

struct AilaCaptureViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> AilaCaptureView {
let view = AilaCaptureView()
view.videoGravity = .resizeAspectFill
return view
}

func updateUIView(_ uiView: AilaCaptureView, context: Context) {
uiView.videoGravity = .resizeAspectFill
}
}

Try changing​

  • Show license.middleNames and license.suffix separately instead of joined into a full name.
  • Compare license.expirationDate to today and flag expired licenses.
  • Remove the licensing check and observe what parse() returns without the entitlement.

API used​

Next: capture card images on a kiosk with ID Tray.


Want the code? The full Xcode project is available as a zip on the Quickstarts overview.