Skip to main content

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()
}
MethodDescription
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
)
ParameterTypeDefaultDescription
isMrzReadingEnabledBoolfalseWhen true, the SDK will only capture documents with a valid MRZ. When false, it captures any document.
isDismissButtonEnabledBoolfalseWhen 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​

  1. Camera Permissions: Always check camera authorization status before starting capture
  2. License Validation: Validate license loading before initializing the SDK
  3. Error Recovery: Provide clear error messages and retry options
  4. User Feedback: Use the onLoading delegate method to show processing states
  5. 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.