|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
import LocalAuthentication import SwiftUI @Observable final class BiometricAuthManager { enum AuthError: LocalizedError { case notAvailable case notEnrolled case cancelled case failed(Error) var errorDescription: String? { switch self { case .notAvailable: return "生体認証がこのデバイスで利用できません。" case .notEnrolled: return "生体認証が設定されていません。設定アプリで登録してください。" case .cancelled: return "認証がキャンセルされました。" case .failed(let error): return "認証に失敗しました: \(error.localizedDescription)" } } } /// 認証成功かどうか var isUnlocked: Bool = false /// エラーメッセージ表示用 var authErrorMessage: String? = nil var showAuthError: Bool = false // MARK: - 認証実行 /// 生体認証(Touch ID / Face ID)+端末パスコード fallback func authenticate() async { let context = LAContext() // パスコード fallback を許可 context.localizedFallbackTitle = "パスコードを使用" var policyError: NSError? // deviceOwnerAuthentication = 生体認証 + パスコード fallback guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &policyError) else { await handleError(policyError) return } do { let success = try await context.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: "メモにアクセスするために認証が必要です" ) await MainActor.run { isUnlocked = success } } catch let error as LAError { await handleLAError(error) } catch { await MainActor.run { authErrorMessage = AuthError.failed(error).errorDescription showAuthError = true } } } // MARK: - ロック func lock() { isUnlocked = false } // MARK: - Private helpers private func handleError(_ error: NSError?) async { await MainActor.run { guard let error else { authErrorMessage = AuthError.notAvailable.errorDescription showAuthError = true return } switch LAError.Code(rawValue: error.code) { case .biometryNotAvailable: authErrorMessage = AuthError.notAvailable.errorDescription case .biometryNotEnrolled: authErrorMessage = AuthError.notEnrolled.errorDescription default: authErrorMessage = AuthError.failed(error).errorDescription } showAuthError = true } } private func handleLAError(_ error: LAError) async { await MainActor.run { switch error.code { case .userCancel, .appCancel, .systemCancel: // キャンセルはエラー表示しない(ロック画面のままにする) isUnlocked = false case .userFallback: // パスコード fallback は .deviceOwnerAuthentication で自動処理されるため通常は来ない isUnlocked = false default: authErrorMessage = AuthError.failed(error).errorDescription showAuthError = true } } } } |
Info.plist に必須キーを追加
|
1 2 |
<key>NSFaceIDUsageDescription</key> <string>メモへのアクセスを保護するために Face ID を使用します</string> |
コメント