Back to Blog

Use Face ID or Touch ID in your SwiftUI App

This Tutorial showcases how to add Biometric Authentication (Face ID, Touch ID) to your iOS App built in Swift and SwiftUI.

Posted by

Thumbnail

One of the main reasons why you would want to use Face ID within your application would be to protect sensitive information of your users from unwanted attention.

Enabling it is straightforward, but requires a couple of steps. But first, let set out the goals of this tutorial. What do we want to achieve?

  • We want to protect sensitive views with Biometric Auth and
  • we want to protect performing sensitive operations with Biometric Auth

First, add a Usage Description for Biometric Authentication in your project's Info.plist:

NSFaceIDUsageDescription → We will use FaceID to authenticate you

Then, let’s create a function that will perform the biometric authentication and return a boolean result, depending on if it’s successful or not.

For that, we’ll create a new class called BiometricAuth, that we will use to call authentication operation.

Here, we’ll create a function called authenticate():

import LocalAuthentication

public class BiometricAuth {
    static public func authenticate() async -> Bool {
        let context = LAContext()
        var error: NSError?

        // Check whether authentication is possible
        if context.canEvaluatePolicy(
          .deviceOwnerAuthentication, error: &error
        ) {
            do {
                // Return the result of the authentication
                return try await context
                  .evaluatePolicy(
                    .deviceOwnerAuthentication, 
                    localizedReason: "Authentication Required."
                  )
            } catch {
		        // Handle your error here
                print("Unhandled biometric auth err: \(error.localizedDescription ?? "Unknown")")
                return false
            }
        } else {
            // No Password or Biometrics to auth -> return true
            return true
        }
    }
}

Congratulations! Now you can check for authentication by calling let success = await BiometricAuth.authenticate() in your code!

In this function, we first check if the device is even capable of performing local authentication methods. That means it must have a password, a paired apple watch or a set up Face ID / Touch ID. If the user doesn’t have any security measures enabled on the device, we will simply return “true”, meaning “authenticated”.

If we are can evaluate authentication on the device, we await the evaluation and return its result.

In the next section, we will add some convenience wrappers around it.

Execute code after a successful or failed Face ID Authentication

For example, we often would want to call the method to authenticate and then execute A or B depending on the success of the authentication. To use our method in this scenario, we could use something like this:

// in BiometricAuth {}
static public func executeIfSuccessfulAuth(
  _ onSuccessClosure: () -> Void, 
  otherwise onFailedClosure: (() -> Void)? = nil
) async {
  guard await authenticate() else {
      if let onFailedClosure {
          onFailedClosure()
      }
      return
  }
  onSuccessClosure()
}

You can pass closures to this function that will execute either in case of a successful authentication or if the authentication fails, like this:

await BiometricAuth.executeIfSuccessfulAuth {
    print("Successful Auth!")
} otherwise: {
    print("Failed Auth!")
}

Note, that the `onFailedClosure` is optional, and you can just pass the `onSuccessClosure`

Protect SwiftUI Views with Biometric Authentication

Now, we will also create our own custom SwiftUI modifier to protect sensitive views from unwanted attention.

struct SenstivieViewModifier: ViewModifier {

    @State private var hideView: Bool = true
    @Environment(\.scenePhase) var scenePhase

    // biometrics changes scenePhase to inactive. we need to address that when tracking changes in scenePhase
    @State private var performingBiometricsRightNow: Bool = false

    func tryToUnlock() {
        Task {
            performingBiometricsRightNow = true
            await BiometricAuth.executeIfSuccessfulAuth {
                withAnimation {
                    hideView = false
                }
            }
            // We add this delay because when we perform a biometric auth operation, the
            // state of the view changes to "not active". Even after it's completed, the view still
            // takes around 2 secs to become "active" again.
            try? await Task.sleep(for: .seconds(2.5))
            performingBiometricsRightNow = false

        }
    }

    func body(content: Content) -> some View {
        content
          .frame(maxWidth: .infinity, 
                  maxHeight: .infinity)
          .overlay {
              if hideView {
                  VStack {
                      Image(systemName: "lock.fill")
                          .foregroundStyle(Color.secondary)
                          .font(
                            .system(
                              size: 75, 
                              weight: .semibold
                            )
                          )
                          .padding(.bottom, 10)

                      Text("Private View")
                          .font(.largeTitle)
                          .bold()

                      Text("Open the App and authenticate to unlock.")
                          .font(.callout)
                          .multilineTextAlignment(.center)
                          .foregroundStyle(.secondary)

                      Button(
                          action: {
                              tryToUnlock()
                          },
                          label: {
                              Text("Unlock")
                          }
                      )
                      .padding(.top)

                  }
                  .frame(maxWidth: .infinity, 
                          maxHeight: .infinity)
                  .background(.thickMaterial)
                  .ignoresSafeArea()
              }
          }
          .task(id: scenePhase) {
              if performingBiometricsRightNow { return }
              withAnimation {
                  if scenePhase == .active && hideView {
                      tryToUnlock()
                  } else {
                      hideView = true
                  }
              }
          }
          .onDisappear {
              // we want to lock the view every time its not visible
              hideView = true
          }
    }
}

This view modifier adds an overlay to the view it is attached to. This overlay is shown until the user performs a biometric authentication (automatically starts it when the view is active or by pressing the Unlock button). Once the view is inactive again, the overlay is shown again. This means that this view is also hidden in the app switcher. Cool!

Showcase of the SwiftUI Views being locked until authenticated

You can attach it to any SwiftUI view like this:

ContentView()
	.modifier(SenstivieViewModifier())

or create a wrapper view modifier:

extension View {
    /// Protects a view with the requirement of having to  
    /// biometrically authenticate in order to see the view.
    /// Also hides the view in the app switcher.
    public func sensitiveView() -> some View {
        modifier(SenstivieViewModifier())
    }
}

that you can call like this

ContentView()
	.sensitiveView()

Hope it helped you! If you have any questions you can always DM me on X (Twitter)

Having to manually build features like this and many others can be avoided by using SwiftyLaunch. You can simply select many features that you might need in your next app, like Auth, In App Purchases, Analytics and more, and SwiftyLaunch will generate a brand new app for you on the fly.

It also contains many useful convenience features, like the ones present in this tutorial. But instead of having to go through the trouble of writing them yourself, you can focus on working on what makes your app actually unique! Stop writing repetetive code and launch your next app on the App Store faster than ever by using SwiftyLaunch.

PS. SwiftyLaunch is currently on sale! Don’t miss out on this opportunity to save big and ship your apps faster than ever.

SwiftyLaunch logoSwiftyLaunch
Skip the tedious setup. Focus on your core features.Generate an iOS Project with these features already pre-made for you:
Buy once, build unlimited apps.