NFC Reader
The capabilities of the Anyline ID plugin can also be used together with our NFC Reader API to read passport data. The NFC reader can be used on supported iPhone models (iPhone 7 and above) running iOS 13 and above.
Setting up a project for NFC Tag Reading
In order to read NFC tags in your project, certain setup steps are needed.
Add NFC Capability
You will need to add the 'Near Field Communication Tag Reading' capability to your project, and to your provisioning profile. Adding capabilities is described on Apple’s help page.
Add Info.plist keys
To allow NFC in your app, you will need to add the following keys to your Info.plist:
-
com.apple.developer.nfc.readersession.iso7816.select-identifiers
: An array containing the string ‘A0000002471001’, which is the application identifier for Machine Readable Travel Documents -
NFCReaderUsageDescription
: A human-readable string explaining what you will be using NFC for (e.g. 'Please allow NFC access to read passports')
Checking for NFC Availability
Since NFC tag reading is only available from iOS 13 onwards, you will need to surround your NFC code with an API availability check:
-
Swift
-
Objective-C
if #available(iOS 13.0, *) {
let nfcDetector = ALNFCDetector(delegate: self)
// ... start using NFC functionality
} else {
// No NFC support
}
if (@available(iOS 13.0, *)) {
ALNFCDetector *NFCDetector = [[ALNFCDetector alloc] initDelegate:self];
// ... start using NFC functionality
} else {
// No NFC support
}
In addition, as there are some iPhones which do not support NFC tag reading ability, and
yet are running on iOS 13 or above, you may need to also check the return value of
[ALNFCDetector readingAvailable]
and disable your app’s NFC UI entirely if NFC reading is
not available on the device.
If you attempt to use NFC on a device that does not support it, the
delegate’s nfcFailedWithError: method will be passed an error object with the code ALNFCTagErrorNFCNotSupported .
|
Getting the MRZ key
To decrypt the data from your NFC chip, you will need some additional information from a passport document: The document number, date of birth and the document date of expiration. You can easily obtain this information by scanning your passport using the MRZ plugin. We recommend making sure that you have a correct result, since incorrect information will make it impossible to decrypt the data from the NFC chip of your ID. You can achieve this by setting a high minConfidence for the necessary fields in your MRZ config.
{
"pluginConfig": {
"id": "passport",
"mrzConfig": {
"mrzMinFieldConfidences": {
"documentNumber": 90,
"dateOfBirth": 90,
"dateOfExpiry": 90,
}
}
}
}
Starting NFC reading
Once you have the required information, you can start reading a tag using the startNfcDetectionWithPassportNumber:dateOfBirth:expirationDate:
method on the ALNFCDetector class.
ALNFCDetector *NFCDetector = [[ALNFCDetector alloc] initDelegate:self];
[NFCDetector startNfcDetectionWithPassportNumber:passportNumberForNFC dateOfBirth:dateOfBirth expirationDate:dateOfExpiry]
This will show the standard iOS user interface for NFC reading, guiding the user and showing what’s happening as the NFC chip is being read.
If the passport number in your MRZ string has a <
after it, you need to include that in the passport number passed into the NFC detector. You can do this using the code below:
-
Swift
-
Objective-C
var passportNumberForNFC = passportNumber
let passportNumberRange = (mrzString as NSString).range(of: mrzString)
if passportNumberRange.location != NSNotFound {
if (mrzString as NSString).character(at: NSMaxRange(passportNumberRange)) == '<' {
passportNumberForNFC.append("<")
}
}
NSMutableString *passportNumberForNFC = [passportNumber mutableCopy];
NSRange passportNumberRange = [mrzString rangeOfString:passportNumber];
if (passportNumberRange.location != NSNotFound) {
if ([mrzString characterAtIndex:NSMaxRange(passportNumberRange)] == '<') {
[passportNumberForNFC appendString:@"<"];
}
}
Receiving the Results
To receive information read from the NFC tag, you need to set a delegate on the ALNFCDetector object which conforms to the ALNFCDetectorDelegate
protocol. When all data from the NFC tag has been read successfully, the - nfcSucceededWithResult:
method is called.
-
Swift
-
Objective-C
@available(iOS 13.0, *)
func nfcSucceeded(with nfcResult: ALNFCResult) {
DispatchQueue.main.async { [weak self] in
self?.displayDataGroup1(nfcResult.dataGroup1)
self?.displayDataGroup2(nfcResult.dataGroup2)
}
}
- (void)nfcSucceededWithResult:(ALNFCResult *)nfcResult {
dispatch_async(dispatch_get_main_queue(), ^{
[self displayDataGroup1:nfcResult.dataGroup1];
[self displayDataGroup2:nfcResult.dataGroup2];
});
}
See below for example implementations of displayDataGroup1:
and displayDataGroup2:
.
You can optionally also be notified when individual data groups have been read from the NFC chip, so that you can display them immediately without waiting for the rest of the data to be read.
If you display data immediately in these methods, you may choose not to redisplay them in nfcSucceededWithResult:
, and instead simply indicate that the reading finished successfully.
-
Swift
-
Objective-C
func nfcSucceeded(with dataGroup1: ALDataGroup1) {
self.displayDataGroup1(dataGroup1)
}
func nfcSucceeded(with dataGroup2: ALDataGroup2) {
self.displayDataGroup2(dataGroup2)
}
- (void)nfcSucceededWithDataGroup1:(ALDataGroup1 *_Nonnull)dataGroup1 {
[self displayDataGroup1:dataGroup1];
}
- (void)nfcSucceededWithDataGroup2:(ALDataGroup2 *_Nonnull)dataGroup2 {
[self displayDataGroup2:dataGroup2];
}
The DataGroup1
object contains textual and date information, while the DataGroup2
object contains the face image. Reading the Data Security Object (SOD) for verifying the authenticity of the data is not yet supported.
One way of implementing displayDataGroup1:
and displayDataGroup2:
as called above is as follows:
-
Swift
-
Objective-C
func displayDataGroup1(_ dataGroup1: ALDataGroup1) {
self.textView.text = "Document Type: \(dataGroup1.documentType)\n\Issuing State Code: \(dataGroup1.issuingStateCode)\nDocument Number: \(dataGroup1.documentNumber)\nDate of Expiry: \(dataGroup1.dateOfExpiry)\nGender: \(dataGroup1.gender)\nNationality: \(dataGroup1.nationality)\nLast Name: \(dataGroup1.lastName)\nFirst Name: \(dataGroup1.firstName)\nDate of Birth: \(dataGroup1.dateOfBirth)"
}
func displayDataGroup2(_ dataGroup2: ALDataGroup2) {
self.imageView.image = dataGroup2.faceImage
}
- (void)displayDataGroup1:(ALDataGroup1 *)dataGroup1 {
self.textView.text = [NSString stringWithFormat:@"Document Type: %@\nIssuing State Code: %@\nDocument Number: %@\nDate of Expiry: %@\nGender: %@\nNationality: %@\nLast Name: %@\nFirst Name: %@\nDate of Birth: %@",
dataGroup1.documentType,
dataGroup1.issuingStateCode,
dataGroup1.documentNumber,
dataGroup1.dateOfExpiry,
dataGroup1.gender,
dataGroup1.nationality,
dataGroup1.lastName,
dataGroup1.firstName,
dataGroup1.dateOfBirth];
[self.textView setHidden:false];
Error Handling
When there is an error reading the NFC chip, the nfcFailedWithError:
method of the delegate is called. The error passed into this method will have an ALErrorCode
from the ALNFCTagError
range, a localizedDescription
, and potentially additional information in the description key of the userInfo
dictionary.
-
Swift
-
Objective-C
func nfcFailedWithError(_ error: Error) {
DispatchQueue.main.async { [weak self] in
// perhaps give the user an option to try reading NFC again
self?.displayError(error)
}
}
- (void)nfcFailedWithError:(NSError * _Nonnull)error {
dispatch_async(dispatch_get_main_queue(), ^{
[self displayError:error];
// perhaps give the user an option to try reading NFC again
});
}
In most cases, the built-in UI already gives some indication to the user that something has gone wrong, so you might choose to use the error information only for troubleshooting, and not display it prominently to users. However, if you have run NFC detection without first checking [ALNFCDetector readingAvailable], you should check for the error code ALNFCTagErrorNFCNotSupported and inform the user that their device does not support NFC in that case. So one way of implementing the displayError: method called above is as follows:
-
Swift
-
Objective-C
func displayError(_ error: NSError) {
print("Failed to read NFC chip. Error: \(error.localizedDescription) (more info: \(error.userInfo[@"description"])");
if error.code == ALNFCTagErrorNFCNotSupported {
self.showAlertWithTitle("NFC not supported",
message: "NFC passport reading is not supported on this device.")
}
}
- (void)displayError:(NSError * _Nonnull)error {
NSLog(@"Failed to read NFC chip. Error: %@ (more info: %@)",error.localizedDescription,error.userInfo[@"description"]);
if (error.code == ALNFCTagErrorNFCNotSupported) {
[self showAlertWithTitle:@"NFC Not Supported" message:@"NFC passport reading is not supported on this device."];
}
}
NFC reading can fail because the information passed into startNfcDetectionWithPassportNumber:dateOfBirth:expirationDate:
was incorrect, or for some other reason, such as the user moving the phone away from the passport, or there being multiple NFC chips nearby. If you want to try reading the NFC chip again, you can use the error code to determine whether to retry with the same details, or scan the MRZ again to try with different details. The error code ALNFCTagErrorResponseError
can indicate the details are incorrect.
Full Example Code
The following code example first scans the passport info page containing MRZ information, and then issues a request to start scanning the passport using the iOS NFC tag reader.
Once the NFC data is fully scanned, the results are printed out on the console.
-
Swift
-
Objective-C
import Anyline
@available(iOS 13.0, *)
class ALNFCScanViewController: UIViewController {
var nfcDetector: ALNFCDetector!
var scanView: ALScanView!
var scanViewPlugin: ALScanViewPlugin!
var scanViewConfig: ALScanViewConfig!
var hintView: UIView?
// keep the last values we read from the MRZ, so we can retry reading NFC if
// NFC failed for reasons other than getting these details wrong
var passportNumberForNFC: String!
var dateOfBirth: Date!
var dateOfExpiry: Date!
override func viewDidLoad() {
super.viewDidLoad()
let NFCEnabled = self.checkNFCCapability()
if !NFCEnabled {
return;
}
self.nfcDetector = try! ALNFCDetector(delegate: self)
let jsonFilePath = Bundle.main.path(forResource: "mrz_nfc_config", ofType: "json")
let configStr = try! String(contentsOfFile: jsonFilePath!, encoding: .utf8)
let JSONConfigObj = configStr.asJSONObject()
// instead of directly creating a scan view plugin, we can let a factory determine
// whether the result is a plain ALScanViewPlugin or an ALViewPluginComposite.
self.scanViewPlugin = try! ALScanViewPluginFactory.withJSONDictionary(JSONConfigObj!) as! ALScanViewPlugin
self.scanViewConfig = try! ALScanViewConfig(jsonDictionary: JSONConfigObj!)
// Tweak the MRZ tolerances here if necessary.
self.configureScanViewConfig()
self.scanViewPlugin.scanPlugin.delegate = self
self.scanView = try! ALScanView(frame: .zero, scanViewPlugin: self.scanViewPlugin, scanViewConfig: self.scanViewConfig)
// add the scan view to the view hierarchy and position it correctly using
// Autolayout.
self.installScanView(self.scanView)
self.scanView.startCamera()
self.view.addSubview(self.scanView)
self.view.sendSubviewToBack(self.scanView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
try! self.scanViewPlugin.start()
}
func checkNFCCapability() -> Bool {
// NOTE: Apart from the iOS version + device requirement, also make sure to
// check that the app was provisioned with the Near Field Communications
// Tag Reading capability.
if ALNFCDetector.readingAvailable() {
return true
}
// NFC not supported!
return false
}
}
@available(iOS 13.0, *)
extension ALNFCScanViewController: ALScanPluginDelegate {
func scanPlugin(_ scanPlugin: ALScanPlugin, resultReceived scanResult: ALScanResult) {
// get passport number, date of birth, date of expiry
let mrzResult = scanResult.pluginResult.mrzResult
let dateOfBirth = type(of: self).formattedStringToDate(mrzResult?.dateOfBirthObject)
let dateOfExpiry = type(of: self).formattedStringToDate(mrzResult?.dateOfExpiryObject)
var passportNumberForNFC = passportNumber
let passportNumberRange = (mrzString as NSString).range(of: mrzString)
if passportNumberRange.location != NSNotFound {
if (mrzString as NSString).character(at: NSMaxRange(passportNumberRange)) == "<" {
passportNumberForNFC.append("<")
}
}
if passportNumberForNFC.count > 0 {
self.scanViewPlugin.stop()
self.hintView?.isHidden = true
DispatchQueue.main.async { [weak self] in
self?.nfcDetector.startNfcDetection(withPassportNumber: passportNumberForNFC,
dateOfBirth: dateOfBirth,
expirationDate: dateOfExpiry)
}
}
self.dateOfBirth = dateOfBirth
self.dateOfExpiry = dateOfExpiry
self.passportNumberForNFC = passportNumberForNFC
}
}
@available(iOS 13.0, *)
extension ALNFCScanViewController: ALNFCDetectorDelegate {
/// This method is called after all the data has been read from the NFC chip. To display
/// data as it is read instead of waiting until everything has been read, we could also
/// implement nfcSucceededWithDataGroup1: or nfcSucceededWithDataGroup2:
func nfcSucceeded(with nfcResult: ALNFCResult) {
// First name: nfcResult.dataGroup1.firstName
// Last name: nfcResult.dataGroup1.lastName
// Date of Birth: nfcResult.dataGroup1.dateOfBirth
// Date of Expiry: nfcResult.dataGroup1.dateOfExpiry
// Document Number: nfcResult.dataGroup1.documentNumber
// Issuing State Code: nfcResult.dataGroup1.issuingStateCode
// Gender: nfcResult.dataGroup1.gender
// Nationality: nfcResult.dataGroup1.nationality
// Face Image: nfcResult.dataGroup2.faceImage
}
func nfcFailedWithError(_ err: Error) {
let error = err as NSError
print("NFC failed with error code: \(error.code), document number: \(self.passportNumberForNFC)");
// In most cases we don't really need to do anything special here since the NFC UI already shows
// that it failed. We shouldn't get ALNFCTagErrorNFCNotSupported either because we check +readingAvailable
// before even showing NFC, so this is just an example of how else that situation could be handled.
if error.code == ALNFCTagErrorNFCNotSupported {
self.showAlertWithTitle("NFC Not Supported", message: "NFC passport reading is not supported on this device.")
}
if error.code == ALNFCTagErrorResponseError || // error ALNFCTagErrorResponseError can mean the MRZ key was wrong
error.code == ALNFCTagErrorUnexpectedError { // error ALNFCTagErrorUnexpectedError can mean the user pressed the
// 'Cancel' button while scanning. It could also mean the phone lost the connection with the NFC chip because it was moved.
try! self.scanViewPlugin.start()
} else {
// the MRZ details are correct, but something else went wrong. We can try reading the NFC chip again without rescanning the MRZ.
self.nfcDetector.startNfcDetection(withPassportNumber: passportNumberForNFC,
dateOfBirth: dateOfBirth,
expirationDate: dateOfExpiry)
}
}
}
API_AVAILABLE(ios(13.0))
@interface ALNFCScanViewController () <ALNFCDetectorDelegate, ALScanPluginDelegate>
@property (nonatomic, strong) ALNFCDetector *nfcDetector;
@property (nonatomic, strong, nullable) ALScanView *scanView;
@property (nonatomic, strong) ALScanViewPlugin *scanViewPlugin;
@property (nonatomic, strong) ALScanViewConfig *scanViewConfig;
@property (nonatomic, strong, nullable) UIView *hintView;
// keep the last values we read from the MRZ, so we can retry reading NFC if
// NFC failed for reasons other than getting these details wrong
@property (nonatomic, copy) NSString *passportNumberForNFC;
@property (nonatomic, strong) NSDate *dateOfBirth;
@property (nonatomic, strong) NSDate *dateOfExpiry;
@end
@implementation ALNFCScanViewController
- (void)viewDidLoad {
[super viewDidLoad];
BOOL NFCEnabled = [self checkNFCCapability];
if (!NFCEnabled) {
return;
}
if (@available(iOS 13.0, *)) {
self.nfcDetector = [[ALNFCDetector alloc] initWithDelegate:self];
}
NSString *jsonFilePath = [[NSBundle mainBundle] pathForResource:@"mrz_nfc_config"
ofType:@"json"];
NSString *configStr = [NSString stringWithContentsOfFile:jsonFilePath
encoding:NSUTF8StringEncoding
error:NULL];
id JSONConfigObj = [configStr asJSONObject];
// instead of directly creating a scan view plugin, we can let a factory determine
// whether the result is a plain ALScanViewPlugin or an ALViewPluginComposite.
self.scanViewPlugin = (ALScanViewPlugin *)[ALScanViewPluginFactory withJSONDictionary:JSONConfigObj];
self.scanViewConfig = [[ALScanViewConfig alloc] initWithJSONDictionary:JSONConfigObj error:nil];
// Tweak the MRZ tolerances here if necessary.
// [self configureScanViewConfig];
self.scanViewPlugin.scanPlugin.delegate = self;
self.scanView = [[ALScanView alloc] initWithFrame:CGRectZero
scanViewPlugin:self.scanViewPlugin
scanViewConfig:self.scanViewConfig
error:nil];
// add the scan view to the view hierarchy and position it correctly using
// Autolayout.
[self installScanView:self.scanView];
[self.scanView startCamera];
[self.view addSubview:self.scanView];
[self.view sendSubviewToBack:self.scanView];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.scanViewPlugin startWithError:nil];
}
- (BOOL)checkNFCCapability {
// NOTE: Apart from the iOS version + device requirement, also make sure to
// check that the app was provisioned with the Near Field Communications
// Tag Reading capability.
if (@available(iOS 13.0, *)) {
if ([ALNFCDetector readingAvailable]) {
return YES;
}
}
// NFC not supported!
return NO;
}
// MARK: - ALScanPluginDelegate
- (void)scanPlugin:(ALScanPlugin *)scanPlugin resultReceived:(ALScanResult *)scanResult {
// get passport number, date of birth, date of expiry
ALMrzResult *mrzResult = scanResult.pluginResult.mrzResult;
NSDate *dateOfBirth = [self.class formattedStringToDate:mrzResult.dateOfBirthObject];
NSDate *dateOfExpiry = [self.class formattedStringToDate:mrzResult.dateOfExpiryObject];
NSMutableString *passportNumberForNFC = [[mrzResult.documentNumber stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] mutableCopy];
NSRange passportNumberRange = [mrzResult.mrzString rangeOfString:passportNumberForNFC];
if (passportNumberRange.location != NSNotFound) {
if ([mrzResult.mrzString characterAtIndex:NSMaxRange(passportNumberRange)] == '<') {
[passportNumberForNFC appendString:@"<"];
}
}
if (passportNumberForNFC.length > 0) {
[self.scanViewPlugin stop];
self.hintView.hidden = YES;
__weak __block typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.nfcDetector startNfcDetectionWithPassportNumber:passportNumberForNFC
dateOfBirth:dateOfBirth
expirationDate:dateOfExpiry];
});
}
self.dateOfBirth = dateOfBirth;
self.dateOfExpiry = dateOfExpiry;
self.passportNumberForNFC = passportNumberForNFC;
}
// MARK: - ALNFCDetectorDelegate
/// This method is called after all the data has been read from the NFC chip. To display
/// data as it is read instead of waiting until everything has been read, we could also
/// implement nfcSucceededWithDataGroup1: or nfcSucceededWithDataGroup2:
/// - Parameter nfcResult: NFCResult object
- (void)nfcSucceededWithResult:(ALNFCResult * _Nonnull)nfcResult API_AVAILABLE(ios(13.0)) {
// First name: nfcResult.dataGroup1.firstName
// Last name: nfcResult.dataGroup1.lastName
// Date of Birth: nfcResult.dataGroup1.dateOfBirth
// Date of Expiry: nfcResult.dataGroup1.dateOfExpiry
// Document Number: nfcResult.dataGroup1.documentNumber
// Issuing State Code: nfcResult.dataGroup1.issuingStateCode
// Gender: nfcResult.dataGroup1.gender
// Nationality: nfcResult.dataGroup1.nationality
// Face Image: nfcResult.dataGroup2.faceImage
}
- (void)nfcFailedWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {
NSLog(@"NFC failed with error code: %ld, document number: %@", (long)error.code, self.passportNumberForNFC);
// In most cases we don't really need to do anything special here since the NFC UI already shows
// that it failed. We shouldn't get ALNFCTagErrorNFCNotSupported either because we check +readingAvailable
// before even showing NFC, so this is just an example of how else that situation could be handled.
if (error.code == ALNFCTagErrorNFCNotSupported) {
[self showAlertWithTitle:@"NFC Not Supported"
message:@"NFC passport reading is not supported on this device."
completion:nil];
}
if (error.code == ALNFCTagErrorResponseError || // error ALNFCTagErrorResponseError can mean the MRZ key was wrong
error.code == ALNFCTagErrorUnexpectedError) { // error ALNFCTagErrorUnexpectedError can mean the user pressed the
// 'Cancel' button while scanning. It could also mean the phone lost the connection with the NFC chip because it was moved.
[self.scanViewPlugin startWithError:nil];
} else {
// the MRZ details are correct, but something else went wrong. We can try reading the NFC chip again without rescanning the MRZ.
[self.nfcDetector startNfcDetectionWithPassportNumber:self.passportNumberForNFC
dateOfBirth:self.dateOfBirth
expirationDate:self.dateOfExpiry];
}
}
@end