Document Reading - OCR Implementation
This guide details the implementation of the Document Reading SDK for iOS. The SDK provides a complete document capture solution with automatic MRZ (Machine Readable Zone) detection and extraction.
Architecture Overview
The iOS Document Reading SDK follows a view controller-based architecture that integrates seamlessly with UIKit:
-
Ready-to-Use UI: The SDK provides
MBDocumentAutoCaptureViewController, a complete view controller with built-in camera handling and document detection. -
Delegate Pattern: Results are delivered via delegate methods, making it easy to integrate with your navigation flow.
-
Automatic MRZ Detection: The SDK automatically detects and extracts MRZ data from travel documents.
Core Components
MBDocumentAutoCaptureViewController
The main view controller that handles document capture. It manages the camera session, document detection, and MRZ extraction automatically.
import MobaiDocument
import AVFoundation
let license = try LicenseHelper.loadLicense()
let configuration = MBDocumentCaptureOptions(isMrzReadingEnabled: true)
let captureViewController = MBDocumentAutoCaptureViewController(
license: license,
configuration: configuration
)
MBDocumentAutoCaptureDelegate
The delegate protocol that handles capture results:
protocol MBDocumentAutoCaptureDelegate {
func onDocumentCaptured(result: MBDocumentCaptureResult)
func onLoading(isPresented: Bool)
func onDismissAction()
}
| Method | Description |
|---|---|
onDocumentCaptured(result:) | Called when a document is successfully captured and MRZ is extracted |
onLoading(isPresented:) | Called to show/hide loading indicators during processing |
onDismissAction() | Called when the user dismisses the capture screen |
Configuration Options
MBDocumentCaptureOptions
Configure the behavior of the document capture interface:
let configuration = MBDocumentCaptureOptions(
isMrzReadingEnabled: true,
isDismissButtonEnabled: false
)
| Parameter | Type | Default | Description |
|---|---|---|---|
isMrzReadingEnabled | Bool | false | When true, the SDK will only capture documents with a valid MRZ. When false, it captures any document. |
isDismissButtonEnabled | Bool | false | When true, shows a dismiss button allowing users to cancel the capture process. |
Complete Integration Example
Step 1: License Loading Helper
Create a helper to load the license from the app bundle:
import Foundation
enum LicenseHelper {
static func loadLicense() throws -> Data {
guard let url = Bundle.main.url(forResource: "iengine", withExtension: "lic") else {
throw LicenseError.missingLicenseFile
}
do {
return try Data(contentsOf: url)
} catch {
throw LicenseError.unreadableLicenseFile(underlying: error)
}
}
}
enum LicenseError: LocalizedError {
case missingLicenseFile
case unreadableLicenseFile(underlying: Error)
var errorDescription: String? {
switch self {
case .missingLicenseFile:
return "Missing iengine.lic in the app bundle."
case .unreadableLicenseFile(let underlying):
return "Unable to read iengine.lic (\(underlying.localizedDescription))."
}
}
}
Step 2: Document Capture Router
Create a router class to manage the document capture flow:
import UIKit
import AVFoundation
import MobaiDocument
final class DocumentCaptureRouter: MBDocumentAutoCaptureDelegate {
let navigationController: UINavigationController
var onCaptureResult: ((MBDocumentCaptureResult) -> Void)?
private var controller: MBDocumentAutoCaptureViewController?
private let titleText: String
init(
title: String = "Capture Document",
navigationController: UINavigationController,
configuration: MBDocumentCaptureOptions = MBDocumentCaptureOptions(
isMrzReadingEnabled: true,
isDismissButtonEnabled: false
),
license: Data
) {
self.navigationController = navigationController
self.titleText = title
self.controller = MBDocumentAutoCaptureViewController(
license: license,
configuration: configuration
)
}
func start() {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
startCapture()
case .notDetermined:
requestCameraAccess()
case .denied, .restricted:
presentCameraDeniedAlert()
@unknown default:
presentCameraDeniedAlert()
}
}
private func requestCameraAccess() {
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
DispatchQueue.main.async {
guard let self else { return }
granted ? self.startCapture() : self.presentCameraDeniedAlert()
}
}
}
private func startCapture() {
guard let controller else { return }
controller.title = titleText
controller.delegate = self
navigationController.pushViewController(controller, animated: true)
}
private func presentCameraDeniedAlert() {
navigationController.topViewController?.presentOkAlert(
title: "Camera access required",
message: "Enable Camera access in Settings to scan documents."
)
}
// MARK: - MBDocumentAutoCaptureDelegate
func onDocumentCaptured(result: MBDocumentCaptureResult) {
onCaptureResult?(result)
}
func onLoading(isPresented: Bool = false) {
DispatchQueue.main.async { [weak controller] in
controller?.onLoading(isPresented: isPresented)
}
}
func onDismissAction() {
// Handle dismiss action if needed
}
}
Step 3: Using the Router
Integrate the router into your flow:
class DocumentFlowCoordinator {
private let navigationController: UINavigationController
private let license: Data
init(navigationController: UINavigationController, license: Data) {
self.navigationController = navigationController
self.license = license
}
func startDocumentCapture() {
let configuration = MBDocumentCaptureOptions(isMrzReadingEnabled: true)
let captureRouter = DocumentCaptureRouter(
title: "Capture",
navigationController: navigationController,
configuration: configuration,
license: license
)
captureRouter.onCaptureResult = { [weak self] result in
self?.handleCaptureResult(result)
}
captureRouter.start()
}
private func handleCaptureResult(_ result: MBDocumentCaptureResult) {
// Extract MRZ data
guard let machineReadableZone = result.machineReadableZone,
let travelDocumentType = result.documentType else {
// Handle error - MRZ not found
return
}
// Use MBDcoumentResultFactory to extract individual fields
let documentNumber = MBDcoumentResultFactory.getDocumentNumber(
machineReadableZone: machineReadableZone,
travelDocumentType: travelDocumentType
)
let dateOfExpiry = MBDcoumentResultFactory.getDateOfExpiry(
machineReadableZone: machineReadableZone,
travelDocumentType: travelDocumentType
)
let dateOfBirth = MBDcoumentResultFactory.getDateOfBirth(
machineReadableZone: machineReadableZone,
travelDocumentType: travelDocumentType
)
// Proceed with NFC reading or other next steps
// ...
}
}
Data Extraction
MBDocumentCaptureResult
The result object containing captured document information:
struct MBDocumentCaptureResult {
let machineReadableZone: String? // The full MRZ string
let documentType: TravelDocumentType? // Type of document (passport, ID card, etc.)
// Additional document data...
}
MBDcoumentResultFactory
Helper class to extract individual fields from the MRZ:
// Extract document number
let documentNumber = MBDcoumentResultFactory.getDocumentNumber(
machineReadableZone: machineReadableZone,
travelDocumentType: travelDocumentType
)
// Extract expiry date (format: YYMMDD)
let dateOfExpiry = MBDcoumentResultFactory.getDateOfExpiry(
machineReadableZone: machineReadableZone,
travelDocumentType: travelDocumentType
)
// Extract date of birth (format: YYMMDD)
let dateOfBirth = MBDcoumentResultFactory.getDateOfBirth(
machineReadableZone: machineReadableZone,
travelDocumentType: travelDocumentType
)
Complete Flow Example
Combining Document Reading with NFC
Here's a complete example that combines document reading with NFC reading:
import UIKit
import SwiftUI
import CoreNFC
import MobaiNfc
import MobaiDocument
final class OCRNfcCaptureFlow: MBDocumentReadingServiceNfcDelegate {
private let navigationController: UINavigationController
private let license: Data
private var captureRouter: DocumentCaptureRouter?
private var lastResults: MBResults?
private lazy var nfcReadingExecutor: MBDocumentReadingServiceNfc = {
let executor = MBDocumentReadingServiceNfc(license: license)
executor.delegate = self
return executor
}()
init(navigationController: UINavigationController, license: Data) {
self.navigationController = navigationController
self.license = license
}
func start() {
let configuration = MBDocumentCaptureOptions(isMrzReadingEnabled: true)
let captureRouter = DocumentCaptureRouter(
title: "Capture",
navigationController: navigationController,
configuration: configuration,
license: license
)
self.captureRouter = captureRouter
captureRouter.start()
captureRouter.onCaptureResult = { [weak self] result in
self?.handleCaptureResult(result)
}
}
private func handleCaptureResult(_ result: MBDocumentCaptureResult) {
guard let mbResults = createResult(from: result) else { return }
guard ensureNfcAvailable() else { return }
lastResults = mbResults
startNfcReading(with: mbResults)
}
private func createResult(from captureResult: MBDocumentCaptureResult) -> MBResults? {
guard let machineReadableZone = captureResult.machineReadableZone,
let travelDocumentType = captureResult.documentType else {
navigationController.topViewController?.presentOkAlert(
title: "Unable to read machine readable zone from document.",
message: ""
) { [weak self] in
self?.navigationController.popViewController(animated: true)
}
return nil
}
return MBResults(
documentNumber: MBDcoumentResultFactory.getDocumentNumber(
machineReadableZone: machineReadableZone,
travelDocumentType: travelDocumentType
),
dateOfExpiry: MBDcoumentResultFactory.getDateOfExpiry(
machineReadableZone: machineReadableZone,
travelDocumentType: travelDocumentType
),
dateOfBirth: MBDcoumentResultFactory.getDateOfBirth(
machineReadableZone: machineReadableZone,
travelDocumentType: travelDocumentType
)
)
}
private func ensureNfcAvailable() -> Bool {
guard NFCTagReaderSession.readingAvailable else {
navigationController.topViewController?.presentOkAlert(
title: "NFC unavailable",
message: "NFC is not available on this device."
)
return false
}
return true
}
private func startNfcReading(with results: MBResults) {
_ = nfcReadingExecutor.execute(result: results)
}
// MARK: - MBDocumentReadingServiceNfcDelegate
func onReadingStarted() { }
func onReadingFinished(result: MBNfcReadingResult) {
let vc = UIHostingController(rootView: MBNfcReadingResultView(result: result))
navigationController.pushViewController(vc, animated: true)
}
func onReadingError(message: String) {
let alert = UIAlertController(
title: "Something went wrong",
message: message,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
if let lastResults {
alert.addAction(UIAlertAction(title: "Retry", style: .default) { [weak self] _ in
_ = self?.startNfcReading(with: lastResults)
})
}
navigationController.topViewController?.present(alert, animated: true)
}
}
SwiftUI Integration
SwiftUI Wrapper
Create a SwiftUI wrapper for the document capture flow:
import SwiftUI
struct DocumentOCRNfcEntryView: View {
var body: some View {
if #available(iOS 16.0, *) {
content
.toolbar(.hidden, for: .navigationBar)
} else {
content
.navigationBarHidden(true)
}
}
private var content: DocumentOCRNfcFlowView {
DocumentOCRNfcFlowView()
}
}
struct DocumentOCRNfcFlowView: UIViewControllerRepresentable {
@Environment(\.dismiss) private var dismiss
func makeUIViewController(context: Context) -> UINavigationController {
let navigationController = UINavigationController(rootViewController: makeRootController())
navigationController.delegate = context.coordinator
return navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
context.coordinator.onDismiss = { dismiss() }
context.coordinator.startIfNeeded(in: uiViewController)
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
final class Coordinator: NSObject, UINavigationControllerDelegate {
private var hasStarted = false
private var flow: OCRNfcCaptureFlow?
var onDismiss: (() -> Void)?
private var didStartFlow = false
func startIfNeeded(in navigationController: UINavigationController) {
guard !hasStarted else { return }
hasStarted = true
guard let license = loadLicense(in: navigationController) else { return }
let flow = OCRNfcCaptureFlow(navigationController: navigationController, license: license)
self.flow = flow
flow.start()
didStartFlow = true
}
private func loadLicense(in navigationController: UINavigationController) -> Data? {
do {
return try LicenseHelper.loadLicense()
} catch {
navigationController.topViewController?.presentOkAlert(
title: "License missing",
message: error.localizedDescription
)
return nil
}
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard didStartFlow, navigationController.viewControllers.count == 1 else { return }
onDismiss?()
}
}
private func makeRootController() -> UIViewController {
let root = UIViewController()
root.view.backgroundColor = .systemBackground
return root
}
}
Error Handling
Camera Permission Handling
Always check and request camera permissions before starting capture:
func start() {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
startCapture()
case .notDetermined:
requestCameraAccess()
case .denied, .restricted:
presentCameraDeniedAlert()
@unknown default:
presentCameraDeniedAlert()
}
}
MRZ Extraction Errors
Handle cases where MRZ cannot be extracted:
guard let machineReadableZone = result.machineReadableZone,
let travelDocumentType = result.documentType else {
navigationController.topViewController?.presentOkAlert(
title: "Unable to read machine readable zone from document.",
message: "Please ensure the document is clearly visible and try again."
) { [weak self] in
self?.navigationController.popViewController(animated: true)
}
return
}
Best Practices
- Camera Permissions: Always check camera authorization status before starting capture
- License Validation: Validate license loading before initializing the SDK
- Error Recovery: Provide clear error messages and retry options
- User Feedback: Use the
onLoadingdelegate method to show processing states - Navigation Management: Properly handle navigation stack when integrating with existing flows
Sample Code
For complete working examples and reference implementations, see the iOS SDK Samples repository on GitHub.