Quick Setup
After completing the installation steps you are ready to read NFC documents. The overall process is: construct a document key from the MRZ data printed on the document, then use one of two approaches to initiate the scan and receive a result.
Document key
Section titled “Document key”OzoneNFCDocumentKey is the access credential that unlocks the NFC chip. It is derived from the Machine Readable Zone (MRZ) printed on the document.
let key = OzoneNFCDocumentKey( passportNumber: "AB1234567", // 9-character document number from MRZ dateOfBirth: "900115", // yyMMdd — e.g. 15 Jan 1990 expiryDate: "260731" // yyMMdd — e.g. 31 Jul 2026)Date fields use the yyMMdd format (2-digit year, 2-digit month, 2-digit day), matching the MRZ encoding defined in ICAO Doc 9303.
Both dates are validated before scanning starts: if either is not a real calendar date in yyMMdd format, the read fails immediately with OzoneNFCError.invalidNfcKeyFormatting — no NFC session is opened. Validate or correct your MRZ data before constructing the key.
Approach 1 — Async/Await with OzoneNFCReader
Section titled “Approach 1 — Async/Await with OzoneNFCReader”OzoneNFCReader provides a headless async API. The system NFC scanning sheet is presented automatically; your code resumes when reading completes or throws.
import OzoneNFC
let reader = OzoneNFCReader()
do { let document = try await reader.readDocument(key) // handle document} catch let error as OzoneNFCError { // handle known error} catch { // handle unexpected error}This is the simplest integration — the system NFC scanning sheet is managed automatically by the OS, so you only need to handle the result.
Custom display messages
Section titled “Custom display messages”Pass a displayMessage closure to override the text shown during reading:
let document = try await reader.readDocument(key) { message in switch message { case .requestPresentPassport: return "Hold your phone against the passport cover." case .authenticatingWithPassport(let progress): return "Authenticating… \(progress)%" case .readingDataGroupProgress(let group, let progress): return "Reading \(group)… \(progress)%" case .successfulRead: return "Done!" default: return nil // use default text }}Approach 2 — Built-in UI with OzoneNFCViewController
Section titled “Approach 2 — Built-in UI with OzoneNFCViewController”OzoneNFCViewController is a UIViewController that bundles a full guided scanning UI. Use it when you want the built-in visual flow.
import OzoneNFC
let vc = OzoneNFCViewController(documentKey: key) { result in switch result { case .success(let document): // handle document case .failure(let error): // handle error }}present(vc, animated: true)Custom display messages
Section titled “Custom display messages”OzoneNFCViewController accepts the same optional displayMessage closure as OzoneNFCReader. Pass it to the initializer to override the text shown during scanning:
let vc = OzoneNFCViewController(documentKey: key, displayMessage: { message in switch message { case .requestPresentPassport: return "Hold your phone against the passport cover." case .authenticatingWithPassport(let progress): return "Authenticating… \(progress)%" case .readingDataGroupProgress(let group, let progress): return "Reading \(group)… \(progress)%" case .successfulRead: return "Done!" default: return nil // use default text }}) { result in // handle result}present(vc, animated: true)SwiftUI
Section titled “SwiftUI”Wrap OzoneNFCViewController in a UIViewControllerRepresentable:
import OzoneNFCimport SwiftUI
struct NFCScanView: UIViewControllerRepresentable { let documentKey: OzoneNFCDocumentKey let completion: (Result<OzoneNFCDocument, OzoneNFCError>) -> Void
func makeUIViewController(context: Context) -> OzoneNFCViewController { OzoneNFCViewController(documentKey: documentKey, completion: completion) }
func updateUIViewController(_ uiViewController: OzoneNFCViewController, context: Context) {}}Present it using a NavigationLink or .fullScreenCover.
Reading the result
Section titled “Reading the result”OzoneNFCDocument is the object returned on a successful read.
Always populated:
| Property | Type | Description |
|---|---|---|
firstName |
String |
Given name(s) |
lastName |
String |
Surname |
documentNumber |
String |
Document number |
documentType |
String |
Raw document type code (e.g. "P") |
documentSubType |
String |
Document sub-type as encoded in the MRZ |
documentCode |
String |
Full document code string from the MRZ |
translatedDocumentType |
TranslatedDocumentType |
Enum — see below |
dateOfBirth |
String |
yyMMdd |
expiryDate |
String |
yyMMdd |
gender |
String |
Sex as encoded in the MRZ — typically "M", "F", or "<" (unspecified) |
nationality |
String |
ISO 3166-1 alpha-3 country code |
issuingAuthority |
String |
Name or code of the authority that issued the document |
passportDataValid |
Bool |
true if all data group hashes match the SOD |
Optional:
| Property | Type | Description |
|---|---|---|
isExpired |
Bool? |
Calculated from expiryDate when the result is created; true if expired, false if not, nil if the date cannot be parsed |
age |
Int? |
Calculated from dateOfBirth; nil if the date cannot be parsed |
personalNumber |
String? |
Personal number from the MRZ (field 14), if present |
placeOfBirth |
String? |
Place of birth if present on the chip |
image |
UIImage? |
Facial image from DG2 |
signatureImage |
UIImage? |
Signature image from DG7, if present |
Security status fields
Section titled “Security status fields”Each authentication step exposes an AuthStatus: .success, .failure, or .skipped (not attempted, for example because the chip doesn’t support it).
| Property | Description |
|---|---|
BACStatus |
Basic Access Control |
PACEStatus |
Password Authenticated Connection Establishment |
chipAuthenticationStatus |
Chip Authentication (anti-cloning) |
activeAuthenticationStatus |
Active Authentication (anti-substitution) |
A failed security step doesn’t fail the read. If the chip fails Chip Authentication or Active Authentication, the read still completes and returns the document data, with that step’s status set to .failure. Check these statuses, together with passportDataValid, before you rely on the result.
TranslatedDocumentType
Section titled “TranslatedDocumentType”public enum TranslatedDocumentType: String { case `default` case nationalPassport case emergencyPassport case diplomaticPassport case officialOrServicePassport case refugeePassport case alienPassport case statelessPassport case travelDocument case militaryPassport}Chip position helper
Section titled “Chip position helper”OzoneNFCReader exposes a static helper to determine where the NFC chip is located on a document, which can be useful for displaying guidance to the user:
let position = OzoneNFCReader.getChipPositionFor(countryCode: "GBR")Returns a value describing the expected chip location for the given ISO 3166-1 alpha-3 country code, or nil if the location is unknown.
Error handling
Section titled “Error handling”OzoneNFCError covers the expected failure cases:
| Case | Description |
|---|---|
.invalidNfcTag |
The tag found is not a valid eMRTD chip |
.invalidNfcKeyFormatting |
The document key (MRZ data) is malformed — including a dateOfBirth or expiryDate that is not a valid yyMMdd date; raised before any NFC session is opened |
.userCancelled |
The user dismissed the NFC sheet |
.unexpectedError |
An unrecoverable internal error occurred |