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.