How to Build an iOS MRZ and VIN Scanner App in Swift

The Dynamsoft GitHub page provides sample projects for learning iOS development with MRZ (Machine Readable Zone) and VIN (Vehicle Identification Number) scanning. Both sample projects are built using Storyboard. In this tutorial, we’ll use the official source code as a foundation to build an iOS application that supports both MRZ and VIN scanning using SwiftUI. iOS MRZ/VIN Scanner Demo Video Prerequisites Free Trial License for Dynamsoft Capture Vision. iOS MRZ Scanner: Includes both a framework and an application project. The framework wraps low-level APIs of Dynamsoft Capture Vision and provides a camera view, while the app demonstrates how to use the framework to scan MRZ and display results. iOS VIN Scanner: Demonstrates how to scan VINs using Dynamsoft Capture Vision. Make sure to request a license key and download both projects before getting started. Setting Up the iOS MRZ/VIN Scanner Project in Xcode Create a new Xcode project using the App template and select SwiftUI as the interface. Go to File > Add Package Dependencies and add all required dependencies from the capture-vision-spm repository. Drag the MRZ scanner framework project into your new project’s Project Navigator. In your project settings under the General tab, locate the Frameworks, Libraries, and Embedded Content section. Ensure that the iOS MRZ Scanner framework is set to Embed & Sign, and verify that all other frameworks are correctly linked. In the Signing & Capabilities tab, enable Automatically manage signing and select your development team. Xcode will automatically generate the signing certificate and provisioning profile for your app. In the Info tab, add a new key for Privacy - Camera Usage Description and provide a description for why your app needs camera access. Build the project to verify that the setup is complete and error-free. UI Design The UI consists of a radio button group for selecting the scan mode (MRZ or VIN), a text area for displaying scan results, and a button to start scanning. The radio button group is implemented using a custom modeButton function, which creates a button with a circular indicator to show the selected mode. The text area is a ScrollView that displays the scan results in a readable format. The start scanning button launches the scanner when tapped. import DynamsoftLicense import DynamsoftMRZScannerBundle import SwiftUI struct ContentView: View { @State private var scanResult: String = "" @State private var scanMode: ScanMode = .mrz var body: some View { VStack(spacing: 16) { // MARK: - Radio Group UI HStack(spacing: 24) { modeButton(title: "MRZ", mode: .mrz) modeButton(title: "VIN", mode: .vin) } .padding(.top, 16) ScrollView { Text(scanResult) .font(.system(size: 20)) .padding(16) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.white) .foregroundColor(.black) .lineSpacing(4) } .background(Color.white) .cornerRadius(8) .shadow(radius: 2) Spacer() HStack(spacing: 8) { Button(action: { presentScanner() }) { Text("START SCANNING") .font(.headline) .foregroundColor(.white) .padding() .frame(maxWidth: .infinity) .background(Color.blue) .cornerRadius(8) } } } .padding(16) .background(Color(UIColor.systemGroupedBackground)) } @ViewBuilder func modeButton(title: String, mode: ScanMode) -> some View { Button(action: { scanMode = mode scanResult = "" }) { HStack(spacing: 8) { ZStack { Circle() .stroke(Color.orange, lineWidth: 2) .frame(width: 20, height: 20) if scanMode == mode { Circle() .fill(Color.orange) .frame(width: 10, height: 10) } } Text(title) .foregroundColor(.black) .font(.system(size: 16, weight: .medium)) } } } } Opening the MRZ and VIN Scanner View The scanner view is implemented using UIKit in the framework project. To integrate it into a SwiftUI app, we create a UIViewControllerRepresentable wrapper around the scanner view controller. The current framework does not support VIN scanning by default, so we will extend it to handle both MRZ

Apr 22, 2025 - 03:54
 0
How to Build an iOS MRZ and VIN Scanner App in Swift

The Dynamsoft GitHub page provides sample projects for learning iOS development with MRZ (Machine Readable Zone) and VIN (Vehicle Identification Number) scanning. Both sample projects are built using Storyboard. In this tutorial, we’ll use the official source code as a foundation to build an iOS application that supports both MRZ and VIN scanning using SwiftUI.

iOS MRZ/VIN Scanner Demo Video

Prerequisites

  • Free Trial License for Dynamsoft Capture Vision.
  • iOS MRZ Scanner: Includes both a framework and an application project. The framework wraps low-level APIs of Dynamsoft Capture Vision and provides a camera view, while the app demonstrates how to use the framework to scan MRZ and display results.
  • iOS VIN Scanner: Demonstrates how to scan VINs using Dynamsoft Capture Vision.

Make sure to request a license key and download both projects before getting started.

Setting Up the iOS MRZ/VIN Scanner Project in Xcode

  1. Create a new Xcode project using the App template and select SwiftUI as the interface.
  2. Go to File > Add Package Dependencies and add all required dependencies from the capture-vision-spm repository.
  3. Drag the MRZ scanner framework project into your new project’s Project Navigator.
  4. In your project settings under the General tab, locate the Frameworks, Libraries, and Embedded Content section. Ensure that the iOS MRZ Scanner framework is set to Embed & Sign, and verify that all other frameworks are correctly linked.

    Xcode framework dependency

  5. In the Signing & Capabilities tab, enable Automatically manage signing and select your development team. Xcode will automatically generate the signing certificate and provisioning profile for your app.

  6. In the Info tab, add a new key for Privacy - Camera Usage Description and provide a description for why your app needs camera access.

  7. Build the project to verify that the setup is complete and error-free.

UI Design

iOS MRZ/VIN Scanner UI

The UI consists of a radio button group for selecting the scan mode (MRZ or VIN), a text area for displaying scan results, and a button to start scanning.

  • The radio button group is implemented using a custom modeButton function, which creates a button with a circular indicator to show the selected mode.
  • The text area is a ScrollView that displays the scan results in a readable format.
  • The start scanning button launches the scanner when tapped.
import DynamsoftLicense
import DynamsoftMRZScannerBundle
import SwiftUI

struct ContentView: View {
    @State private var scanResult: String = ""
    @State private var scanMode: ScanMode = .mrz

    var body: some View {
        VStack(spacing: 16) {
            // MARK: - Radio Group UI
            HStack(spacing: 24) {
                modeButton(title: "MRZ", mode: .mrz)
                modeButton(title: "VIN", mode: .vin)
            }
            .padding(.top, 16)

            ScrollView {
                Text(scanResult)
                    .font(.system(size: 20))
                    .padding(16)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.white)
                    .foregroundColor(.black)
                    .lineSpacing(4)
            }
            .background(Color.white)
            .cornerRadius(8)
            .shadow(radius: 2)

            Spacer()

            HStack(spacing: 8) {
                Button(action: {
                    presentScanner()

                }) {
                    Text("START SCANNING")
                        .font(.headline)
                        .foregroundColor(.white)
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(Color.blue)
                        .cornerRadius(8)
                }
            }
        }
        .padding(16)
        .background(Color(UIColor.systemGroupedBackground))
    }

    @ViewBuilder
    func modeButton(title: String, mode: ScanMode) -> some View {
        Button(action: {
            scanMode = mode
            scanResult = ""  
        }) {
            HStack(spacing: 8) {
                ZStack {
                    Circle()
                        .stroke(Color.orange, lineWidth: 2)
                        .frame(width: 20, height: 20)
                    if scanMode == mode {
                        Circle()
                            .fill(Color.orange)
                            .frame(width: 10, height: 10)
                    }
                }
                Text(title)
                    .foregroundColor(.black)
                    .font(.system(size: 16, weight: .medium))
            }
        }
    }
}

Opening the MRZ and VIN Scanner View

The scanner view is implemented using UIKit in the framework project. To integrate it into a SwiftUI app, we create a UIViewControllerRepresentable wrapper around the scanner view controller. The current framework does not support VIN scanning by default, so we will extend it to handle both MRZ and VIN scanning.


struct ContentView: View {
    ...

    func presentScanner() {
        let config = ScannerConfig()
        config.license =
            "LICENSE-KEY"
        config.mode = scanMode

        var scannerView = MRZScannerView(config: config)
        scannerView.onScannedResult = { result in
            DispatchQueue.main.async {
                switch result.resultStatus {
                case .finished:
                    switch scanMode {
                    case .mrz:
                        let mrzResult = result as? MRZScanResult
                        if let data = mrzResult?.data {
                            self.scanResult +=
                                "Name: " + data.firstName + " " + data.lastName + "\n\n"
                            self.scanResult += "Sex: " + data.sex.capitalized + "\n\n"
                            self.scanResult += "Age: " + String(data.age) + "\n\n"
                            self.scanResult += "Document Type: " + data.documentType + "\n\n"
                            self.scanResult += "Document Number: " + data.documentNumber + "\n\n"
                            self.scanResult += "Issuing State: " + data.issuingState + "\n\n"
                            self.scanResult += "Nationality: " + data.nationality + "\n\n"
                            self.scanResult += "Date Of Birth: " + data.dateOfBirth + "\n\n"
                            self.scanResult += "Date Of Expire: " + data.dateOfExpire + "\n\n"
                        }
                    case .vin:
                        let vinResult = result as? VINScanResult
                        if let data = vinResult?.data {
                            self.scanResult += "VIN String: " + data.vinString + "\n\n"
                            self.scanResult += "WMI: " + data.wmi + "\n\n"
                            self.scanResult += "Region: " + data.region + "\n\n"
                            self.scanResult += "VDS: " + data.vds + "\n\n"
                            self.scanResult += "Check Digit: " + data.checkDigit + "\n\n"
                            self.scanResult += "Model Year: " + data.modelYear + "\n\n"
                            self.scanResult += "Manufacturer plant: " + data.plantCode + "\n\n"
                            self.scanResult += "Serial Number: " + data.serialNumber + "\n\n"
                        }
                    }
                case .canceled:
                    self.scanResult = "Scan canceled"
                case .exception:
                    self.scanResult = result.errorString ?? "Unknown error"
                @unknown default:
                    break
                }

                let rootVC = UIApplication.shared.connectedScenes
                    .compactMap { $0 as? UIWindowScene }
                    .flatMap { $0.windows }
                    .first { $0.isKeyWindow }?.rootViewController

                rootVC?.dismiss(animated: true, completion: nil)
            }
        }

        let rootVC = UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .flatMap { $0.windows }
            .first { $0.isKeyWindow }?.rootViewController

        rootVC?.present(
            UIHostingController(rootView: scannerView),
            animated: true,
            completion: nil
        )
    }
}

struct MRZScannerView: UIViewControllerRepresentable {
    let config: ScannerConfig
    var onScannedResult: ((ScanResultBase) -> Void)?

    func makeUIViewController(context: Context) -> ScannerViewController {
        let vc = ScannerViewController()
        vc.config = config
        vc.onScannedResult = onScannedResult
        return vc
    }

    func updateUIViewController(_ uiViewController: ScannerViewController, context: Context) {}
}

Integrating VIN Scanning Capability into Scanner View

The recognition mode is determined by the template name:

  • MRZ uses ReadPassportAndId
  • VIN uses ReadVINText

When scanning starts, the appropriate template name activates the desired recognition mode.

Defining the Scan Mode

In ScannerConfig.swift, add a new enum representing the scan mode. The mode property is used to determine which template to use for scanning.

public enum ScanMode {
    case mrz
    case vin
}

public class ScannerConfig: NSObject {
    ...
    public var mode: ScanMode = ScanMode.mrz
}

VIN Recognition and Parsing Result

In ScanResult.swift, create a ScanResultBase class that serves as the base class for all scan results. The MRZScanResult and VINScanResult classes inherit from this base class. The VINScanResult class contains properties specific to VIN scanning.

@objcMembers
@objc(DSScanResultBase)
public class ScanResultBase: NSObject {
    public let resultStatus: ResultStatus
    public let errorCode: Int
    public let errorString: String?

    init(resultStatus: ResultStatus, errorCode: Int = 0, errorString: String? = nil) {
        self.resultStatus = resultStatus
        self.errorCode = errorCode
        self.errorString = errorString
    }
}

public class MRZScanResult: ScanResultBase {
    public let data: MRZData?

    init(
        resultStatus: ResultStatus, mrzdata: MRZData? = nil, errorCode: Int = 0,
        errorString: String? = nil
    ) {
        self.data = mrzdata
        super.init(resultStatus: resultStatus, errorCode: errorCode, errorString: errorString)
    }
}

public class VINScanResult: ScanResultBase {
    public let data: VINData?

    init(
        resultStatus: ResultStatus, vindata: VINData? = nil, errorCode: Int = 0,
        errorString: String? = nil
    ) {
        self.data = vindata
        super.init(resultStatus: resultStatus, errorCode: errorCode, errorString: errorString)
    }
}

@objcMembers
@objc(DSVINData)
public class VINData: NSObject {
    public let vinString: String
    public let wmi: String
    public let region: String
    public let vds: String
    public let checkDigit: String
    public let modelYear: String
    public let plantCode: String
    public let serialNumber: String

    init(
        vinString: String, wmi: String, region: String, vds: String, checkDigit: String,
        modelYear: String, plantCode: String, serialNumber: String
    ) {
        self.vinString = vinString
        self.wmi = wmi
        self.region = region
        self.vds = vds
        self.checkDigit = checkDigit
        self.modelYear = modelYear
        self.plantCode = plantCode
        self.serialNumber = serialNumber
    }
}

Handling VIN Recognition in ScannerViewController

When the scanner view is opened, the UI is presented based on the selected scanning mode. If the mode is set to VIN, the result must be parsed and converted into a VINData object.

public override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        dce.open()
        var name: String
        ...

        switch config.mode {
        case .mrz:
            name = "ReadPassportAndId"
        case .vin:
            name = "ReadVINText"
        }
        ...
        cvr.startCapturing(name) { isSuccess, error in
            if let error = error as? NSError, !isSuccess {
                self.onScannedResult?(
                    .init(
                        resultStatus: .exception, errorCode: error.code,
                        errorString: error.localizedDescription))
            }
        }
    }

private func setupUI() {
        ...

        switch config.mode {
        case .mrz:
            imageView.isHidden = !config.isGuideFrameVisible
            imageView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(imageView)

            let safeArea = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                imageView.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor),
                imageView.centerYAnchor.constraint(equalTo: safeArea.centerYAnchor),
                imageView.widthAnchor.constraint(
                    lessThanOrEqualTo: safeArea.widthAnchor, multiplier: 0.9),
                imageView.heightAnchor.constraint(
                    lessThanOrEqualTo: safeArea.heightAnchor, multiplier: 0.9),

                closeButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 20),
                closeButton.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 20),

                stackView.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor),
                stackView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -50),
            ])
        case .vin:
            let region = Rect()
            region.top = 0.4
            region.bottom = 0.6
            region.left = 0.1
            region.right = 0.9
            region.measuredInPercentage = true
            try? dce.setScanRegion(region)
        }
    }

extension ScannerViewController: CapturedResultReceiver {

    public func onParsedResultsReceived(_ result: ParsedResult) {
        guard let item = result.items?.first else { return }
        stop()
        if config.isBeepEnabled {
            Feedback.beep()
        }
        switch config.mode {
        case .mrz:
            let mrzdata = convertToMRZData(item: item)
            let data = MRZScanResult(resultStatus: .finished, mrzdata: mrzdata)
            onScannedResult?(data)
        case .vin:
            let vindata = convertToVINData(item: item)
            let data = VINScanResult(resultStatus: .finished, vindata: vindata)
            onScannedResult?(data)
        }
    }

    private func convertToVINData(item: ParsedResultItem) -> VINData? {
        let parsedFields = item.parsedFields

        guard let vinString = parsedFields["vinString"] == nil ? "N/A" : parsedFields["vinString"],
            let wmi = parsedFields["WMI"] == nil ? "N/A" : parsedFields["WMI"],
            let region = parsedFields["region"] == nil ? "N/A" : parsedFields["region"],
            let vds = parsedFields["VDS"] == nil ? "N/A" : parsedFields["VDS"],
            let checkDigit = parsedFields["checkDigit"] == nil ? "N/A" : parsedFields["checkDigit"],
            let modelYear = parsedFields["modelYear"] == nil ? "N/A" : parsedFields["modelYear"],
            let plantCode = parsedFields["plantCode"] == nil ? "N/A" : parsedFields["plantCode"],
            let serialNumber = parsedFields["serialNumber"] == nil
                ? "N/A" : parsedFields["serialNumber"]
        else { return nil }

        let vinData = VINData(
            vinString: vinString, wmi: wmi, region: region, vds: vds,
            checkDigit: checkDigit, modelYear: modelYear, plantCode: plantCode,
            serialNumber: serialNumber)
        return vinData
    }
}

Final Scanner View for VIN Recognition

The final scanner view for VIN recognition is similar to the MRZ scanner view. However, it does not display the guide frame. In setupUI(), the guide frame is hidden when the scan mode is set to VIN, and a rectangular scan region is defined instead.

iOS VIN Scanner

Source Code

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