# BiometricAuthentication: Face ID, Touch ID and Optic ID without the singleton

> This Swift package wraps LocalAuthentication so an iOS app can prompt for Face ID, Touch ID or Optic ID, fall back to the device passcode, and localise its error messages. Version 4 is a rewrite: async and await, no shared instance, a Swift Package, and a stale prompt fix for the case where your app asks twice.

**rushisangani/BiometricAuthentication** — Use Apple FaceID or TouchID authentication in your app using BiometricAuthentication.

- Repository: https://github.com/rushisangani/BiometricAuthentication
- Stars: 829 · Forks: 113
- Language: Swift
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/rushisangani-biometricauthentication

## The 4.0 rewrite changed the shape of the API

Version 4.0.0 was a rewrite rather than an increment, and the README lists what changed. The two main entry points, authenticateWithBiometrics and authenticateWithPasscode, are now async throws and simply return on success, while the completion-handler variants are still available, are called on the main actor and receive a Result<Void, AuthenticationError>. Several names were made more Swift-like, so authenticateWithBioMetrics becomes authenticateWithBiometrics, isFaceIdDevice() becomes isFaceIDDevice(), and AuthenticationError.initWithError(_:) becomes AuthenticationError(_:). The structural change is the removal of the singleton. Instead of reaching for BioMetricAuthenticator.shared you create and keep your own BiometricAuthenticator, and the README is explicit that you should keep it as a property and reuse it. Version 4 also added Optic ID support for Apple Vision Pro, a privacy manifest in the shape of PrivacyInfo.xcprivacy, DocC documentation, and unit tests written with Swift Testing.

## One authenticator, one prompt, and why a second call cancels the first

The 4.1.0 release is a small one with a specific target, and the mechanism it fixes is the kind of bug that only shows up on a device. The problem was that starting a new authentication invalidated the one still in progress on the same authenticator, which failed with canceledByApp, and repeated calls, for example each time the app became active, left stale prompts behind. The fix is described as one prompt at a time. The related rule in the usage notes is that only one authentication runs at a time per authenticator, and starting a new one invalidates the one still in progress, which then fails with AuthenticationError.canceledByApp. This is worth internalising before you wire it into a view that appears on foreground. If your app calls authenticateWithBiometrics when it becomes active and the user has already started a prompt, you will get a cancellation error rather than a second prompt. The practical answer is to hold one authenticator as a property and decide at the call site whether a new attempt makes sense.

## Error cases are mapped to Apple's LAError names

The error surface widened in version 4 to cover canceledByApp, invalidContext, notInteractive and companionNotAvailable, matching Apple's LAError. The companionNotAvailable case is the one that matters for a specific scenario: Apple Watch authentication on a newer iPhone relies on an authenticated paired watch, and if that pairing is not available the authentication cannot proceed, so you need to handle it rather than treat it as a generic failure. AuthenticationError conforms to LocalizedError, so error.localizedDescription returns a readable message. The messages themselves are looked up in your own bundle and strings table, either a .strings or a .xcstrings file, with built-in English defaults for any missing key. When the user denies the app's Face ID permission, biometryNotAvailable shows a Face ID message that points to Settings, using the faceid.not.available key. The consequence for an app with non-English users is that you supply the translations and the package supplies the lookup mechanism.

## allowableReuseDuration skips the prompt, but only for lock screen unlocks

The reusable authentication feature is the most easily misused part of the API. The authenticator has an allowableReuseDuration property in seconds, defaulting to 0 which means no reuse. If the user unlocked the device with Face ID or Touch ID on the lock screen within that window, authenticateWithBiometrics succeeds immediately without showing a prompt. Two limits matter. Only lock-screen unlocks are reused. A previous biometric authentication inside your app, or in another app, is never reused, so calling authenticateWithBiometrics twice in a row prompts twice. And the maximum is five minutes, taken from LATouchIDAuthenticationMaximumAllowableReuseDuration, with larger values clamped. The setting does not apply to authenticateWithPasscode. The README is also blunt about verification: this can only be checked on a real device, by locking the device, unlocking with Face ID or Touch ID, opening the app and authenticating within the duration, because the simulator cannot simulate a lock-screen biometric unlock.

## Adding the package and the Info.plist key

The package supports both Swift Package Manager and CocoaPods. In Xcode you use File then Add Package Dependencies and enter the repository URL, or you add it to Package.swift yourself:

```swift
dependencies: [
    .package(url: "https://github.com/rushisangani/BiometricAuthentication.git", from: "4.1.0")
]
```

The CocoaPods route is a one-liner in the Podfile:

```ruby
pod 'BiometricAuthentication', '~> 4.0'
```

There is one thing you must do yourself, and the README puts it in a note at the top because forgetting it produces a denial rather than an error. Face ID authentication requires the user's permission to be declared in Info.plist, so you add the usage description key:

```xml
<key>NSFaceIDUsageDescription</key>
<string>This app requires Face ID permission to authenticate using Face recognition.</string>
```

The requirements are iOS 15.0 or newer and Xcode 15 or newer, which means Swift 5.9, and the package works in both the Swift 5 and Swift 6 language modes. Xcode 16 or newer is needed to run the package's own unit tests, since they use Swift Testing. The repository ships a podspec, a Package.swift, Sources, Tests, a CHANGELOG.md and an Example directory with both SwiftUI and UIKit example projects.

## Checking capability first, and the two API shapes

Before prompting, you can check whether the device supports Touch ID, Face ID or Optic ID and whether your app can use it right now:

```swift
if BiometricAuthenticator.canAuthenticate() {
    try await authenticator.authenticateWithBiometrics(reason: "")
}
```

The authenticator itself is created with the bundle and strings table used for message lookup, and you can set the reuse window on it:

```swift
import BiometricAuthentication

// Localized messages are looked up in the given bundle and strings table (defaults: `.main`, "Localizable").
let authenticator = BiometricAuthenticator(bundle: .main, tableName: "Localizable")

// Optional: reuse a recent lock-screen unlock (in seconds) without prompting again. Default is 0 (no reuse).
authenticator.allowableReuseDuration = 30
```

An empty reason string is not an error. When the reason is empty the default for the device is used, and the README gives the two defaults, Confirm your face to authenticate. for Face ID devices and Confirm your fingerprint to authenticate. for Touch ID devices. From UIKit the async call has to be wrapped in a Task, because authentication runs on the main actor:

```swift
@IBAction func biometricAuthenticationClicked(_ sender: Any) {
    Task {
        do {
            try await authenticator.authenticateWithBiometrics(reason: "")
        } catch {
            // handle error
        }
    }
}
```

## Passcode fallback is what makes it usable in practice

The feature list includes device passcode authentication to unlock biometry after multiple failed attempts, which is the part that turns a biometric prompt from a wall into a fallback. iOS locks out biometric authentication after a number of failures and falls back to the device passcode, and an app that only offers authenticateWithBiometrics will be stuck at that point. Having authenticateWithPasscode as a sibling call means you can offer the passcode path yourself when the biometric one fails, which is both better behaviour and something the platform forces you to handle. The functions only throw AuthenticationError, but the README notes that because Swift 5 throws are untyped a final catch is still required, so the do/catch pattern in the examples has a generic branch at the end. The example code shows the shape:

```swift
do {
    try await authenticator.authenticateWithBiometrics(reason: "")
    print("Authentication Successful")
} catch let error as AuthenticationError {
    print(error.message(using: authenticator))
} catch {
    print("Authentication Failed")
}
```

The error.message(using:) call takes the authenticator, which is how the localised text and the localised lookup are tied together.

## Two releases in a day after a six year gap

The maintenance picture has two halves and both matter. The recent release list shows v4.1.0 and v4.0.0 both on 2026-10-13, minutes apart, with v4.1.0 titled as a stale prompt fix, a Face ID permission message and Swift 6. Before those, the previous release was v3.1.3 from 2020-10-14. So the project was quiet for roughly six years and then shipped two major releases on the same day. The repository is not archived and the last push was on 2026-10-13, matching the release. The licence is MIT with the LICENSE file at the root. For an adopter the practical reading is that the 4.x line is new and the 3.x tutorials, examples and Stack Overflow answers that predate it are about an API that has since been replaced. The README points to a Migrating from 3.x section for the API changes, and that section is the one to read if you are following an older guide or if you have an existing app on the singleton-based API.

## Conclusion

Adopt BiometricAuthentication if you are building an iOS app that needs biometric unlock with passcode fallback and you want localised error messages without writing the LocalAuthentication plumbing yourself. Do not adopt it if you are targeting anything below iOS 15, since the minimum deployment target moved to iOS 15.0 in version 4, and do not expect the simulator to prove the lock-screen reuse behaviour works, because the README says that can only be verified on a real device. Before you commit, add the NSFaceIDUsageDescription key to your Info.plist, decide whether you want the instance-based API or the older shared singleton, and read the migration notes for 3.x before following an older tutorial.

## FAQ

### What is BiometricAuthentication?

It is an MIT licensed Swift package for using Apple Face ID, Touch ID or Optic ID authentication in an iOS app. It handles biometric and device passcode authentication based on the device, with async and completion handler APIs and localisable error messages.

### How do I install BiometricAuthentication?

Through Swift Package Manager, either with File then Add Package Dependencies in Xcode or by adding .package(url: "https://github.com/rushisangani/BiometricAuthentication.git", from: "4.1.0") to Package.swift. CocoaPods is also supported with pod 'BiometricAuthentication', '~> 4.0'.

### Why does my Face ID prompt fail with canceledByApp?

Only one authentication runs at a time per authenticator, so starting a new one invalidates the one still in progress, and the earlier call fails with AuthenticationError.canceledByApp. Version 4.1.0 was released specifically to stop repeated calls leaving stale prompts behind.

### Does allowableReuseDuration work in the iOS simulator?

No. The README states it can only be verified on a real device, by locking the device, unlocking it with Face ID or Touch ID, opening the app and authenticating within the duration. Only lock-screen unlocks are reused, and the maximum value is five minutes.

## Sources

- [Issues](https://github.com/rushisangani/BiometricAuthentication/issues)
- [License: MIT](https://github.com/rushisangani/BiometricAuthentication/blob/master/LICENSE)
- [README](https://github.com/rushisangani/BiometricAuthentication/blob/master/README.md)
- [Releases](https://github.com/rushisangani/BiometricAuthentication/releases)
- [rushisangani/BiometricAuthentication on GitHub](https://github.com/rushisangani/BiometricAuthentication)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rushisangani-biometricauthentication
