Разработка авторизации по биометрии (Face ID) в iOS-приложении
Face ID в iOS работает через Local Authentication framework — конкретно через LAContext и метод evaluatePolicy(_:localizedReason:reply:). Звучит просто, пока не начинаешь разбираться с обработкой ошибок, fallback-сценариями и поведением на устройствах без Face ID.
На ревью в App Store приложения с некорректной биометрией заворачивают по гайдлайну 5.1.1 (Privacy) — если reason string не объясняет пользователю, зачем нужен доступ, или если .biometryNotAvailable приводит к краш-петле вместо graceful degradation.
Где чаще всего ошибаются
Самая частая проблема — вызов evaluatePolicy на main thread без проверки canEvaluatePolicy. Приложение зависает на 0.5–1 секунду в момент инициализации LAContext, если устройство только что заблокировалось. На iPhone 14 Pro это незаметно, на iPhone SE 2nd gen — ощутимо.
Вторая проблема — неправильная обработка LAError. У ошибки пять состояний, которые требуют разного UX: .userCancel, .userFallback, .systemCancel, .biometryLockout, .biometryNotAvailable. Разработчики часто сваливают всё в один catch и показывают generic "ошибка авторизации". Пользователь после трёх неудачных попыток Face ID получает lockout — биометрия блокируется до ввода пасскода. Приложение обязано это обработать и предложить fallback, иначе пользователь просто застрянет.
Третья — хранение токенов после успешной биометрии. Нередко вижу, как access token кладут в UserDefaults. Правильно — Keychain с атрибутом kSecAttrAccessControl, созданным через SecAccessControlCreateWithFlags с флагом .biometryCurrentSet или .userPresence. При смене биометрии (добавление нового пальца, перерегистрация лица) .biometryCurrentSet инвалидирует запись автоматически.
Как строим реализацию
Работаем на LAContext с политикой .deviceOwnerAuthenticationWithBiometrics для чистой биометрии или .deviceOwnerAuthentication если нужен fallback на пасскод устройства.
Базовый flow:
- Проверяем
canEvaluatePolicy— получаем тип биометрии черезcontext.biometryType(.faceID,.touchID,.opticIDна Vision Pro). - Запускаем
evaluatePolicyна фоновом потоке (GCD или async/await сTask.detached). - В
reply-блоке обрабатываем все вариантыLAError— каждый отдельным case. - При успехе достаём токен из Keychain через
SecItemCopyMatching.
Для Swift Concurrency-стека оборачиваем LAContext в withCheckedThrowingContinuation. Важный момент: LAContext не является Sendable, поэтому при работе с async/await нужно либо держать его на MainActor, либо использовать @unchecked Sendable с явной синхронизацией.
Keychain-запись с биометрической защитой:
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.biometryCurrentSet,
nil
)
Флаг kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly гарантирует, что данные не попадут в iCloud Backup и не восстановятся на другом устройстве.
Тестирование
Симулятор поддерживает Face ID — через меню Features → Face ID можно эмулировать успех и ошибку. Но .biometryLockout на симуляторе не воспроизводится — тестируем только на физических устройствах. Для UI-тестов используем протокол-обёртку над LAContext, который подменяем mock-объектом в XCTest.
Интеграция с архитектурой приложения
В VIPER и Clean Architecture биометрический модуль выносится в отдельный Interactor (BiometricAuthInteractor) с зависимостью через протокол BiometricServiceProtocol. В SwiftUI + MVVM — как @MainActor-класс, публикующий @Published var authState: AuthState.
Поддерживаем сценарии: первичная регистрация биометрии (пользователь ещё не включил Face ID в настройках приложения), переключение на PIN-код, полное отключение биометрии. Все состояния персистируем в UserDefaults как булевый флаг isBiometricEnabled — не сам токен, только метаданные о выборе пользователя.
Этапы работы
Аудит текущего auth-модуля (если есть) → проектирование сценариев (happy path + все error cases) → разработка сервисного слоя с unit-тестами → интеграция с UI → QA на реальных устройствах (iPhone SE, iPhone 15 Pro, iPad с Face ID) → ревью перед сабмитом в App Store.
Срок реализации с нуля — от 3 до 7 рабочих дней в зависимости от сложности существующей архитектуры и количества точек входа в приложение.







