Dear Folks, today I am going to explain the proper way to integrate the Face ID or Touch ID in iOS App. Face ID and Touch ID are secure, familiar authentication methods that people trust. Follow below steps:
Step 1: Add NSFaceIDUsageDescription Key in info.plist file
1 2 |
<key>NSFaceIDUsageDescription</key> <string>This app requires Face ID permission to authenticate using Face recognition.</string> |
Step 2: Create a swift file with any name but it would be better to related to its functionality, Like BioMetrixFile.swift
Step 3: In BioMetrixFile write below codes and follow instructions. But before proceeding further import LocalAuthentication class in BioMetrixFile.
Now, create typealias for success or failure blocks to well manage Authentication Success or Failure.
1 2 3 |
public typealias AuthenticationSuccess = (() -> ()) public typealias AuthenticationFailure = ((AuthenticationError) -> ()) |
Now written all string messages success or error, created enum for them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
let kBiometryNotAvailableReason = "BioMetrix authentication is not available for this device." enum BioMetrixTouchIDErrors: String { //Touch ID case kTouchIdAuthenticationReason = "Confirm your fingerprint to authenticate." case kTouchIdPasscodeAuthenticationReason = "Touch ID is locked now, because of too many failed attempts. Enter passcode to unlock Touch ID." /// Error Messages Touch ID case kSetPasscodeToUseTouchID = "Please set device passcode to use Touch ID for authentication." case kNoFingerprintEnrolled = "There are no fingerprints enrolled in the device. Please go to Device Settings -> Touch ID & Passcode and enroll your fingerprints." case kDefaultTouchIDAuthenticationFailedReason = "Touch ID does not recognize your fingerprint. Please try again with your enrolled fingerprint." } enum BioMetrixFaceIDErrors: String{ //Face ID case kFaceIdAuthenticationReason = "Confirm your face to authenticate." case kFaceIdPasscodeAuthenticationReason = "Face ID is locked now, because of too many failed attempts. Enter passcode to unlock Face ID." // Error Messages Face ID case kSetPasscodeToUseFaceID = "Please set device passcode to use Face ID for authentication." case kNoFaceIdentityEnrolled = "There is no face enrolled in the device. Please go to Device Settings -> Face ID & Passcode and enroll your face." case kDefaultFaceIDAuthenticationFailedReason = "Face ID does not recognize your face. Please try again with your enrolled face." } |
Step 4: Created a Singleton class with name BioMetrixAuthentication which is subclass of NSObject.
1 2 3 |
open class BioMetrixAuthentication: NSObject { public static let shared = BioMetrixAuthentication() } |
Step 5: Added a class function to check whether device has TouchID available or not.
1 2 3 4 5 6 7 8 9 10 |
class func canAuthenticate() -> Bool { var isBioMetrixAuthenticationAvailable = false var error: NSError? = nil if LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) { isBioMetrixAuthenticationAvailable = (error == nil) } return isBioMetrixAuthenticationAvailable } |
Step 6: Added a function to Check BioMetrix Authentication (FaceId / Touch Id)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class func authenticateWithBioMetrixs(reason: String, fallbackTitle: String? = "", cancelTitle: String? = "", success successBlock:@escaping AuthenticationSuccess, failure failureBlock:@escaping AuthenticationFailure) { let reasonString = reason.isEmpty ? BioMetrixAuthentication.shared.defaultBioMetrixAuthenticationReason() : reason let context = LAContext() context.localizedFallbackTitle = fallbackTitle // cancel button title if #available(iOS 10.0, *) { context.localizedCancelTitle = cancelTitle } // authenticate BioMetrixAuthentication.shared.evaluate(policy: LAPolicy.deviceOwnerAuthenticationWithBiometrics, with: context, reason: reasonString, success: successBlock, failure: failureBlock) } |
Step 7: Add a function to check device passcode if user fails to authenticate himself using touchID or faceID several times.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/// Check for device passcode authentication class func authenticateWithPasscode(reason: String, cancelTitle: String? = "", success successBlock:@escaping AuthenticationSuccess, failure failureBlock:@escaping AuthenticationFailure) { let reasonString = reason.isEmpty ? BioMetrixAuthentication.shared.defaultPasscodeAuthenticationReason() : reason let context = LAContext() // cancel button title if #available(iOS 10.0, *) { context.localizedCancelTitle = cancelTitle } // authenticate if #available(iOS 9.0, *) { BioMetrixAuthentication.shared.evaluate(policy: LAPolicy.deviceOwnerAuthentication, with: context, reason: reasonString, success: successBlock, failure: failureBlock) } else { // Fallback on earlier versions BioMetrixAuthentication.shared.evaluate(policy: LAPolicy.deviceOwnerAuthenticationWithBiometrics, with: context, reason: reasonString, success: successBlock, failure: failureBlock) } } |
Step 8: Add a method to check if device supports faceID or not.
1 2 3 4 5 6 7 |
public func faceIDAvailable() -> Bool { if #available(iOS 11.0, *) { let context = LAContext() return (context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: nil) && context.biometryType == .faceID) } return false } |
Step 9: Add a method to show authentication message regarding faceID or TouchId. If device supports faceID, it will pick the value from BioMetrixFaceIDError Enum otherwise from BioMetrixTouchIDErrors Enum.
1 2 3 |
func defaultBioMetrixAuthenticationReason() -> String { return faceIDAvailable() ? BioMetrixFaceIDErrors.kFaceIdAuthenticationReason.rawValue : BioMetrixTouchIDErrors.kTouchIdAuthenticationReason.rawValue } |
Step 10. Now Add a method to handle the error message, when user has too many failed attempts for faceID or TouchId.
1 2 3 |
func defaultPasscodeAuthenticationReason() -> String { return faceIDAvailable() ? BioMetrixFaceIDErrors.kFaceIdPasscodeAuthenticationReason.rawValue : BioMetrixTouchIDErrors.kTouchIdPasscodeAuthenticationReason.rawValue } |
Step 11. Now Add method to evaluate the specified policy for faceID or TouchID. More info: https://developer.apple.com/documentation/localauthentication/lacontext
1 2 3 4 5 6 7 8 9 10 11 12 |
func evaluate(policy: LAPolicy, with context: LAContext, reason: String, success successBlock:@escaping AuthenticationSuccess, failure failureBlock:@escaping AuthenticationFailure) { context.evaluatePolicy(policy, localizedReason: reason) { (success, err) in DispatchQueue.main.async { if success { successBlock() } else { let errorType = AuthenticationError.initWithError(err as! LAError) failureBlock(errorType) } } } } |
Step 12: Add AuthenticationError enum to manage errors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
public enum AuthenticationError { case failed, canceledByUser, fallback, canceledBySystem, passcodeNotSet, biometryNotAvailable, biometryNotEnrolled, biometryLockedout, other public static func initWithError(_ error: LAError) -> AuthenticationError { switch Int32(error.errorCode) { case kLAErrorAuthenticationFailed: return failed case kLAErrorUserCancel: return canceledByUser case kLAErrorUserFallback: return fallback case kLAErrorSystemCancel: return canceledBySystem case kLAErrorPasscodeNotSet: return passcodeNotSet case kLAErrorBiometryNotAvailable: return biometryNotAvailable case kLAErrorBiometryNotEnrolled: return biometryNotEnrolled case kLAErrorBiometryLockout: return biometryLockedout default: return other } } // get error message based on type public func message() -> String { let authentication = BioMetrixAuthentication.shared switch self { case .canceledByUser, .fallback, .canceledBySystem: return "" case .passcodeNotSet: return authentication.faceIDAvailable() ? BioMetrixFaceIDErrors.kSetPasscodeToUseFaceID.rawValue : BioMetrixTouchIDErrors.kSetPasscodeToUseTouchID.rawValue case .biometryNotAvailable: return kBiometryNotAvailableReason case .biometryNotEnrolled: return authentication.faceIDAvailable() ? BioMetrixFaceIDErrors.kNoFaceIdentityEnrolled.rawValue : BioMetrixTouchIDErrors.kNoFingerprintEnrolled.rawValue case .biometryLockedout: return authentication.faceIDAvailable() ? BioMetrixFaceIDErrors.kFaceIdPasscodeAuthenticationReason.rawValue : BioMetrixTouchIDErrors.kTouchIdPasscodeAuthenticationReason.rawValue default: return authentication.faceIDAvailable() ? BioMetrixFaceIDErrors.kDefaultFaceIDAuthenticationFailedReason.rawValue : BioMetrixTouchIDErrors.kDefaultTouchIDAuthenticationFailedReason.rawValue } } } |
If you followed all the steps mentioned above, your BioMetrixFile.swift will look like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
import UIKit import LocalAuthentication public typealias AuthenticationSuccess = (() -> ()) public typealias AuthenticationFailure = ((AuthenticationError) -> ()) let kBiometryNotAvailableReason = "BioMetrix authentication is not available for this device." enum BioMetrixTouchIDErrors: String { //Touch ID case kTouchIdAuthenticationReason = "Confirm your fingerprint to authenticate." case kTouchIdPasscodeAuthenticationReason = "Touch ID is locked now, because of too many failed attempts. Enter passcode to unlock Touch ID." /// Error Messages Touch ID case kSetPasscodeToUseTouchID = "Please set device passcode to use Touch ID for authentication." case kNoFingerprintEnrolled = "There are no fingerprints enrolled in the device. Please go to Device Settings -> Touch ID & Passcode and enroll your fingerprints." case kDefaultTouchIDAuthenticationFailedReason = "Touch ID does not recognize your fingerprint. Please try again with your enrolled fingerprint." } enum BioMetrixFaceIDErrors: String{ //Face ID case kFaceIdAuthenticationReason = "Confirm your face to authenticate." case kFaceIdPasscodeAuthenticationReason = "Face ID is locked now, because of too many failed attempts. Enter passcode to unlock Face ID." // Error Messages Face ID case kSetPasscodeToUseFaceID = "Please set device passcode to use Face ID for authentication." case kNoFaceIdentityEnrolled = "There is no face enrolled in the device. Please go to Device Settings -> Face ID & Passcode and enroll your face." case kDefaultFaceIDAuthenticationFailedReason = "Face ID does not recognize your face. Please try again with your enrolled face." } open class BioMetrixAuthentication: NSObject { // MARK: - Singleton public static let shared = BioMetrixAuthentication() } // MARK:- Public public extension BioMetrixAuthentication { /// checks if TouchID or FaceID is available on the device. class func canAuthenticate() -> Bool { var isBioMetrixAuthenticationAvailable = false var error: NSError? = nil if LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) { isBioMetrixAuthenticationAvailable = (error == nil) } return isBioMetrixAuthenticationAvailable } /// Check for BioMetrix authentication class func authenticateWithBioMetrixs(reason: String, fallbackTitle: String? = "", cancelTitle: String? = "", success successBlock:@escaping AuthenticationSuccess, failure failureBlock:@escaping AuthenticationFailure) { let reasonString = reason.isEmpty ? BioMetrixAuthentication.shared.defaultBioMetrixAuthenticationReason() : reason let context = LAContext() context.localizedFallbackTitle = fallbackTitle // cancel button title if #available(iOS 10.0, *) { context.localizedCancelTitle = cancelTitle } // authenticate BioMetrixAuthentication.shared.evaluate(policy: LAPolicy.deviceOwnerAuthenticationWithBiometrics, with: context, reason: reasonString, success: successBlock, failure: failureBlock) } /// Check for device passcode authentication class func authenticateWithPasscode(reason: String, cancelTitle: String? = "", success successBlock:@escaping AuthenticationSuccess, failure failureBlock:@escaping AuthenticationFailure) { let reasonString = reason.isEmpty ? BioMetrixAuthentication.shared.defaultPasscodeAuthenticationReason() : reason let context = LAContext() // cancel button title if #available(iOS 10.0, *) { context.localizedCancelTitle = cancelTitle } // authenticate if #available(iOS 9.0, *) { BioMetrixAuthentication.shared.evaluate(policy: LAPolicy.deviceOwnerAuthentication, with: context, reason: reasonString, success: successBlock, failure: failureBlock) } else { // Fallback on earlier versions BioMetrixAuthentication.shared.evaluate(policy: LAPolicy.deviceOwnerAuthenticationWithBiometrics, with: context, reason: reasonString, success: successBlock, failure: failureBlock) } } /// checks if face id is avaiable on device public func faceIDAvailable() -> Bool { if #available(iOS 11.0, *) { let context = LAContext() return (context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: nil) && context.biometryType == .faceID) } return false } } // MARK:- Private extension BioMetrixAuthentication { /// get authentication reason to show while authentication func defaultBioMetrixAuthenticationReason() -> String { return faceIDAvailable() ? BioMetrixFaceIDErrors.kFaceIdAuthenticationReason.rawValue : BioMetrixTouchIDErrors.kTouchIdAuthenticationReason.rawValue } /// get passcode authentication reason to show while entering device passcode after multiple failed attempts. func defaultPasscodeAuthenticationReason() -> String { return faceIDAvailable() ? BioMetrixFaceIDErrors.kFaceIdPasscodeAuthenticationReason.rawValue : BioMetrixTouchIDErrors.kTouchIdPasscodeAuthenticationReason.rawValue } /// evaluate policy func evaluate(policy: LAPolicy, with context: LAContext, reason: String, success successBlock:@escaping AuthenticationSuccess, failure failureBlock:@escaping AuthenticationFailure) { context.evaluatePolicy(policy, localizedReason: reason) { (success, err) in DispatchQueue.main.async { if success { successBlock() } else { let errorType = AuthenticationError.initWithError(err as! LAError) failureBlock(errorType) } } } } } //----- Validations Errors // Success ------ public enum AuthenticationError { case failed, canceledByUser, fallback, canceledBySystem, passcodeNotSet, biometryNotAvailable, biometryNotEnrolled, biometryLockedout, other public static func initWithError(_ error: LAError) -> AuthenticationError { switch Int32(error.errorCode) { case kLAErrorAuthenticationFailed: return failed case kLAErrorUserCancel: return canceledByUser case kLAErrorUserFallback: return fallback case kLAErrorSystemCancel: return canceledBySystem case kLAErrorPasscodeNotSet: return passcodeNotSet case kLAErrorBiometryNotAvailable: return biometryNotAvailable case kLAErrorBiometryNotEnrolled: return biometryNotEnrolled case kLAErrorBiometryLockout: return biometryLockedout default: return other } } // get error message based on type public func message() -> String { let authentication = BioMetrixAuthentication.shared switch self { case .canceledByUser, .fallback, .canceledBySystem: return "" case .passcodeNotSet: return authentication.faceIDAvailable() ? BioMetrixFaceIDErrors.kSetPasscodeToUseFaceID.rawValue : BioMetrixTouchIDErrors.kSetPasscodeToUseTouchID.rawValue case .biometryNotAvailable: return kBiometryNotAvailableReason case .biometryNotEnrolled: return authentication.faceIDAvailable() ? BioMetrixFaceIDErrors.kNoFaceIdentityEnrolled.rawValue : BioMetrixTouchIDErrors.kNoFingerprintEnrolled.rawValue case .biometryLockedout: return authentication.faceIDAvailable() ? BioMetrixFaceIDErrors.kFaceIdPasscodeAuthenticationReason.rawValue : BioMetrixTouchIDErrors.kTouchIdPasscodeAuthenticationReason.rawValue default: return authentication.faceIDAvailable() ? BioMetrixFaceIDErrors.kDefaultFaceIDAuthenticationFailedReason.rawValue : BioMetrixTouchIDErrors.kDefaultTouchIDAuthenticationFailedReason.rawValue } } } |
Now moving to the UIViewController, where you want to integrate the FaceID or TouchID. Create UIButton IBAction method and follow below proper steps to manage the correct flow of authentication. Adding below lines of code in IBAction method of this button.
Steps 13: Starting Authentication by calling the class function of BioMetrixAuthentication Class in BioMetrixFile.swift file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// start authentication BioMetrixAuthentication.authenticateWithBioMetrixs(reason: "", success: { // authentication successful self.showLoginSucessAlert() }, failure: { [weak self] (error) in // do nothing on canceled if error == .canceledByUser || error == .canceledBySystem { return } // device does not support biometric (face id or touch id) authentication else if error == .biometryNotAvailable { print("Show Error: ", error.message()) } // show alternatives on fallback button clicked else if error == .fallback { // here we're entering username and password print("Normal login with username & password, make textfield first responder") } // No biometry enrolled in this device, ask user to register fingerprint or face else if error == .biometryNotEnrolled { self?.showGotoSettingsAlert(message: error.message()) } // Biometry is locked out now, because there were too many failed attempts. // Need to enter device passcode to unlock. else if error == .biometryLockedout { self?.showPasscodeAuthentication(message: error.message()) } // show error on authentication failed else { print("Show Error: ", error.message()) } }) |
Step 14: Add method to show passcode authentication
1 2 3 4 5 6 7 8 9 10 11 |
// show passcode authentication func showPasscodeAuthentication(message: String) { BioMetrixAuthentication.authenticateWithPasscode(reason: message, success: { // passcode authentication success self.showLoginSucessAlert() }) { (error) in print("Show Error: ", error.message()) } } |
Step 15: Adding a common method for login success.
1 2 3 |
func showLoginSucessAlert(){ print("Login Success") } |
Step 16: Handling the situation, when user has not enabled FaceId or TouchId.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func showGotoSettingsAlert(message: String){ let alert = UIAlertController(title: "Go to settings", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (self) in let url = URL(string: "App-Prefs:root=TOUCHID_PASSCODE") if UIApplication.shared.canOpenURL(url!) { UIApplication.shared.open(url!, options: [:], completionHandler: nil) } })) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) self.present(alert, animated: true) } |
I would love to hear from you, if you have any doubt regarding code please leave comment below.