# Face ID Biometric Login Setup with Local App State (SwiftUI)

> By Lawrence Arya, Founder & CEO of VP0. Published 2026-07-01. 6 min read.
> Source: https://vp0.com/blogs/faceid-biometric-login-swiftui

Two concerns people conflate: the biometric check and the app state it unlocks. Here is how each half works, and where the secret really lives.

**TL;DR.** Face ID login in SwiftUI is two parts: the biometric check and the app state it unlocks. LocalAuthentication's LAContext evaluates a policy and returns pass or fail; it never hands you the face, and you never store one. A single in-memory isUnlocked flag gates which view shows and re-locks on background, while the real secret, a session token, lives in the Keychain behind a biometric access control. Keep a passcode fallback and handle the error cases, and the login matches how Apple built the feature. Apple puts the odds of a stranger unlocking Face ID at under 1 in 1,000,000.

Face ID login in SwiftUI is a two-part job that people usually conflate: the biometric check, and the app state it unlocks. The biometric side is Apple's **LocalAuthentication** framework, where an **LAContext** evaluates a policy and returns pass or fail. It never gives you the face, and you never store one. The state side is a single source of truth, an isUnlocked flag driven by SwiftUI state, that gates which view shows and re-locks when the app goes to the background. Get those two right, keep a passcode fallback, and store the real secret in the Keychain rather than in app state, and you have a login that matches how Apple built the feature. Apple puts the odds of a random person unlocking your device with Face ID at [less than 1 in 1,000,000](https://developer.apple.com/design/human-interface-guidelines/face-id-and-touch-id), which is why it is worth doing by the book.

## How does Face ID login actually work in SwiftUI?

You ask an LAContext whether it can evaluate a biometric policy, then ask it to. The [LocalAuthentication framework](https://developer.apple.com/documentation/localauthentication) exposes canEvaluatePolicy to check availability and evaluatePolicy to run the prompt. In practice you call canEvaluatePolicy with deviceOwnerAuthenticationWithBiometrics, and if it returns true you call evaluatePolicy with a localizedReason string that Apple shows in the system sheet. The result is a simple success or an error, and the whole thing is asynchronous, so you flip your isUnlocked state on the main actor when it succeeds.

The reason string matters more than it looks. It is the only text you control in the Face ID prompt, and Apple's [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/face-id-and-touch-id) ask you to say why you need authentication, not how it works. "Unlock your account" is right; "Scan your face with Face ID" is redundant because the system already says that. This is the same design discipline behind [a well-built Face ID login screen](/blogs/face-id-biometric-login-screen-swiftui/).

## Where does the local app state fit?

In one flag, owned by a view model, not scattered across screens. A single isUnlocked boolean, published from an observable object, decides whether the app shows the locked gate or the real content. Drive it from the evaluatePolicy result, and reset it when the app leaves the foreground so a returning user re-authenticates. SwiftUI's scenePhase makes that easy: when the phase moves to background, set isUnlocked to false.

The trap is persisting that flag. An isUnlocked value saved to UserDefaults or @AppStorage means anyone who bypasses the biometric once stays unlocked forever, and it survives reinstalls oddly. Keep the unlocked state in memory only. What you persist is the credential the login protects, and that belongs in the Keychain, not in app state.

## What do you store, and where?

The token, in the Keychain, protected by the biometric itself. The [Keychain Services API](https://developer.apple.com/documentation/security/keychain-services) stores a session token or refresh token with an access control that ties it to biometrics, so the item only comes out after a successful Face ID check. Use an accessibility level like kSecAttrAccessibleWhenUnlockedThisDeviceOnly and a SecAccessControl with biometryCurrentSet, which has a useful property: if the user adds or removes a face or fingerprint, the item invalidates and forces a fresh login. You never store the password, and you never store biometric data, because iOS keeps that in the Secure Enclave where your app cannot reach it.

| Approach | What it prompts | Use when |
| --- | --- | --- |
| deviceOwnerAuthenticationWithBiometrics | Face ID or Touch ID only | fast unlock with your own passcode fallback in code |
| deviceOwnerAuthentication | biometrics, then device passcode | you want the system passcode as the automatic fallback |
| Keychain item with biometryCurrentSet | biometric to release a stored token | the login must survive relaunch without re-entering a password |

## Common Face ID login mistakes

The biggest is treating biometrics as the whole authentication instead of a fast path to it. Face ID unlocks a token that your server still validates; it does not replace the account. If biometrics fail or the device has none enrolled, the email-and-password or passcode path has to be right there, which is also what keeps the app usable on older hardware. The [React Native and Expo Face ID flow](/blogs/react-native-expo-faceid-login/) handles the same fallback split.

Two more come up constantly. Ignoring the error cases from evaluatePolicy, such as biometryLockout after too many failures or userCancel, leaves users stuck with no way forward, so map each to a sensible UI. And skipping enrollment-change handling means a stolen device with a newly added face keeps access. The modern answer to that last problem is [passkeys, which pair a biometric with a synced credential](/blogs/passkey-creation-biometric-ui-mobile/) and remove the shared secret entirely.

## Key takeaways: Face ID login in SwiftUI

- Split the two concerns: LocalAuthentication does the biometric check, a single in-memory isUnlocked flag gates the app state.
- Never persist the unlocked flag; store the session token in the Keychain behind a biometryCurrentSet access control instead.
- Always ship a fallback, either deviceOwnerAuthentication for the system passcode or your own email path, so no one is locked out.
- Write the localizedReason as why, not how, and let the system describe Face ID itself.
- Handle the real errors, lockout, cancel, and enrollment change, so the login degrades gracefully instead of trapping the user.

## Frequently asked questions

**How do I add Face ID login to a SwiftUI app?**
Import LocalAuthentication, create an LAContext, call canEvaluatePolicy with deviceOwnerAuthenticationWithBiometrics to check availability, then evaluatePolicy with a short localizedReason. On success, set an in-memory isUnlocked flag on the main actor to reveal the app, and release the session token from the Keychain. Keep a passcode or password fallback for when biometrics are unavailable.

**Is it safe to store a password behind Face ID?**
Store a session or refresh token, not the password, and put it in the Keychain with a biometric access control rather than in UserDefaults. iOS keeps the actual face and fingerprint data in the Secure Enclave, which your app can never read, so you are only ever gating access to a token the biometric releases.

**What happens if Face ID fails or is not set up?**
You fall back. Either use the deviceOwnerAuthentication policy, which offers the device passcode automatically after biometrics fail, or route users to your own email-and-password screen. evaluatePolicy also returns specific errors like lockout and cancel, and each should lead somewhere useful rather than a dead end.

**Should I re-authenticate when the app returns from the background?**
Yes for anything sensitive. Watch SwiftUI's scenePhase and set isUnlocked to false when the app enters the background, so returning to the foreground shows the lock again. Because the flag lives only in memory, backgrounding naturally forces a fresh Face ID check on return.

**Do passkeys replace Face ID login?**
Passkeys use Face ID as the local gesture but replace the shared password with a synced public-key credential, which is stronger against phishing and database leaks. For a brand-new app, passkeys are worth considering from the start; for an existing token-based login, Face ID over the Keychain remains a solid, simpler pattern.

## Frequently asked questions

### How do I add Face ID login to a SwiftUI app?

Import LocalAuthentication, create an LAContext, call canEvaluatePolicy with deviceOwnerAuthenticationWithBiometrics to check availability, then evaluatePolicy with a short localizedReason. On success, set an in-memory isUnlocked flag on the main actor to reveal the app, and release the session token from the Keychain. Keep a passcode or password fallback for when biometrics are unavailable.

### Is it safe to store a password behind Face ID?

Store a session or refresh token, not the password, and put it in the Keychain with a biometric access control rather than in UserDefaults. iOS keeps the actual face and fingerprint data in the Secure Enclave, which your app can never read, so you are only ever gating access to a token the biometric releases.

### What happens if Face ID fails or is not set up?

You fall back. Either use the deviceOwnerAuthentication policy, which offers the device passcode automatically after biometrics fail, or route users to your own email-and-password screen. evaluatePolicy also returns specific errors like lockout and cancel, and each should lead somewhere useful rather than a dead end.

### Should I re-authenticate when the app returns from the background?

Yes for anything sensitive. Watch SwiftUI's scenePhase and set isUnlocked to false when the app enters the background, so returning to the foreground shows the lock again. Because the flag lives only in memory, backgrounding naturally forces a fresh Face ID check on return.

### Do passkeys replace Face ID login?

Passkeys use Face ID as the local gesture but replace the shared password with a synced public-key credential, which is stronger against phishing and database leaks. For a brand-new app, passkeys are worth considering from the start; for an existing token-based login, Face ID over the Keychain remains a solid, simpler pattern.

---
*Published on the [VP0 Journal](https://vp0.com/blogs). Free to read, index and cite with attribution.*
