NEW: Master Swift design patterns with my latest book! >>

< Previous: Writing somewhere safe: the iOS keychain   Next: Wrap up >

Touch to activate: Touch ID, Face ID and LocalAuthentication

This part of the project requires a biometry-capable device, although I'll be showing you a hack to make it work on the simulator by bypassing checks at two points.

Touch ID and Face ID are part of the Local Authentication framework, and our code needs to three things:

  1. Check whether the device is capable of supporting biometric authentication.
  2. If so, request that the biometry system begin a check now, giving it a string containing the reason why we're asking. For Touch ID the string is written in code; for Face ID the string is written into our Info.plist file.
  3. If we get success back from the authentication request it means this is the device's owner so we can unlock the app, otherwise we show a failure message.

One caveat that you must be careful of: when we're told whether Touch ID/Face ID was successful or not, it might not be on the main thread. This means we need to use async() to make sure we execute any user interface code on the main thread.

Note: Xcode 9 shipped with Face ID quite broken; please use Xcode 9.1 or later to follow along.

The job of task 1 is done by the canEvaluatePolicy() method of the LAContext class, requesting the security policy type .deviceOwnerAuthenticationWithBiometrics. The job of task 2 is done by the evaluatePolicy() of that same class, using the same policy type, but it accepts a trailing closure telling us the result of the policy evaluation: was it successful, and if not what was the reason?

Like I said, all this is provided by the Local Authentication framework, so the first thing we need to do is import that framework. Add this above import UIKit:

import LocalAuthentication

And now here's the new code for the authenticateTapped() method. We already walked through what it does, so this shouldn't be too surprising:

@IBAction func authenticateTapped(_ sender: Any) {
    let context = LAContext()
    var error: NSError?

    if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
        let reason = "Identify yourself!"

        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) {
            [unowned self] (success, authenticationError) in

            DispatchQueue.main.async {
                if success {
                    self.unlockSecretMessage()
                } else {
                    // error
                }
            }
        }
    } else {
        // no biometry
    }
}

A couple of reminders: we need [unowned self] inside the first closure but not the second because it's already unowned by that point. You also need to use self. inside the closure to make capturing clear. Finally, you must provide a reason why you want Touch ID/Face ID to be used, so you might want to replace mine ("Identify yourself!") with something a little more descriptive.

You can see the “Identify yourself!” string in our code, which will be shown to Touch ID users. Face ID is handled slightly differently – open Info.plist, then add a new key called “Privacy - Face ID Usage Description”. This should contain similar text to what you use with Touch ID, so give it the value “Identify yourself!”.

That's enough to get basic biometric authentication working, but there are error cases you need to catch. For example, you’ll hit problems if the device does not have biometric capability or it isn’t configured. Similarly, you’ll get an error if the user failed authentication, which might be because their fingerprint or face wasn't scanning for whatever reason, but also if the system has to cancel scanning for some reason.

To catch authentication failure errors, replace the // error comment with this:

let ac = UIAlertController(title: "Authentication failed", message: "You could not be verified; please try again.", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
self.present(ac, animated: true)

We also need to show an error if biometry just isn't available, so replace the // no Touch ID comment with this:

let ac = UIAlertController(title: "Biometry unavailable", message: "Your device is not configured for biometric authentication.", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
self.present(ac, animated: true)

That completes the authentication code, but before we're done I want to add one more thing before the project is complete: if you don't have a biometry-capable device and you want to make sure the code works OK, you have a few options.

First, the hack: you can effectively disable biometry by changing these two lines:

if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {

if success {

…to read this both times:

if true {

That condition will always evaluate to true, so it will allow you to test the app using older devices.

Alternatively, if you’re using the Simulator there are useful options under the Hardware menu – go to Hardware > Touch ID/Face ID > Toggle Enrolled State to opt in to biometric authentication, then use Hardware > Touch ID/Face ID > Matching Touch/Face when you’re asked for a fingerprint/face.

Go from iOS to macOS the easy way!

If you like Hacking with Swift, you'll love Hacking with macOS – learn to build macOS apps today, using 18 real-world projects!

< Previous: Writing somewhere safe: the iOS keychain   Next: Wrap up >
Click here to visit the Hacking with Swift store >>