Swift Tutorial: Build an iOS MRZ and VIN Barcode Scanner with Dynamsoft Capture Vision
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.
This article is Part 3 in a 3-Part Series.
What you’ll build: A SwiftUI iOS app that scans both MRZ documents (passports and ID cards) and VIN barcodes using the Dynamsoft Capture Vision SDK, with a radio button UI to switch between scan modes at runtime.
Key Takeaways
- Dynamsoft Capture Vision’s
ScannerViewControllercan be wrapped in aUIViewControllerRepresentableto integrate seamlessly into a SwiftUI app with no UIKit boilerplate in the view layer. - MRZ and VIN scanning use different capture templates (
ReadPassportAndIdvs.ReadVINText) — switching between modes at runtime requires only a singleScannerConfig.modeproperty change. - VIN results are parsed from
ParsedResultItem.parsedFieldsinto a strongly-typedVINDatamodel, giving structured access to WMI, VDS, model year, and serial number. - This architecture can be extended to any additional Dynamsoft recognition mode (barcodes, driver’s licences) with minimal structural changes to the scanner framework.
Common Developer Questions
- How do I build an iOS MRZ and VIN barcode scanner in Swift using SwiftUI?
- How do I parse MRZ and VIN data from Dynamsoft Capture Vision results in Swift?
- How do I switch between MRZ and VIN scanning modes at runtime in an iOS app?
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.
Step 1: Set 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.
Step 2: Design the 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
modeButtonfunction, which creates a button with a circular indicator to show the selected mode. - The text area is a
ScrollViewthat displays the scan results in a readable format. - The start scanning button launches the scanner when tapped.
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))
}
}
}
}
Step 3: Open 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) {}
}
Step 4: Integrate VIN Scanning Into the 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.
Define the Scan Mode Enum
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
}
Parse VIN Recognition Results
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
}
}
Handle 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
}
}
Complete the 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.

Common Issues & Edge Cases
- License key not set or expired: If the scanner launches but immediately returns an error, verify your Dynamsoft license key is correctly assigned in
ScannerConfigand has not expired. Request a new trial license via the Dynamsoft website if needed. - VIN fields returning
nil: Some VINs from older vehicles may omit optional fields. TheconvertToVINDatamethod guards against nil by substituting"N/A"— ensure your UI handles these placeholder values gracefully rather than crashing. - Camera permission denied: If the scanner view appears blank, confirm that the
Privacy - Camera Usage Descriptionkey is present in your app’sInfo.plist. iOS silently blocks camera access without this entry.
Source Code
https://github.com/yushulx/ios-swiftui-barcode-mrz-document-scanner/tree/main/examples/MrzVinScanner