SwiftUI使用TouchID和FaceID
SwiftUI使用TouchID和FaceID

SwiftUI使用TouchID和FaceID

在 SwiftUI 中,可以通过集成 LocalAuthentication 框架来使用 Touch ID 和 Face ID。这些功能被统称为 生物识别认证,适用于设备锁定、应用保护、数据安全等场景。

实现步骤

1、导入框架

引入 LocalAuthentication 框架以访问生物识别认证功能。

import LocalAuthentication

2、创建认证逻辑

使用 LAContext 来执行生物识别认证。

3. 集成到 SwiftUI 界面

将认证逻辑绑定到 SwiftUI 的视图中,通过按钮触发或其他条件自动执行。

完整代码示例

以下是一个使用 Touch ID 或 Face ID 的示例应用:

import SwiftUI
import LocalAuthentication

struct ContentView: View {
    @State private var isAuthenticated = false
    @State private var errorMessage = ""
    
    var body: some View {
        VStack(spacing: 20) {
            if isAuthenticated {
                Text("Welcome! 🎉")
                    .font(.largeTitle)
                    .foregroundColor(.green)
            } else {
                Text("Please authenticate")
                    .font(.title)
                    .foregroundColor(.red)
                Button(action: authenticate) {
                    Label("Unlock with Biometrics", systemImage: "faceid")
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .clipShape(Capsule())
                }
                if !errorMessage.isEmpty {
                    Text("Error: \(errorMessage)")
                        .foregroundColor(.red)
                        .font(.footnote)
                }
            }
        }
        .padding()
    }
    
    func authenticate() {
        let context = LAContext()
        var error: NSError?

        // 检查设备是否支持生物识别
        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            let reason = "Authenticate to access your account"

            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
                DispatchQueue.main.async {
                    if success {
                        isAuthenticated = true
                        errorMessage = ""
                    } else {
                        isAuthenticated = false
                        errorMessage = authenticationError?.localizedDescription ?? "Unknown error"
                    }
                }
            }
        } else {
            // 设备不支持生物识别
            DispatchQueue.main.async {
                errorMessage = error?.localizedDescription ?? "Biometric authentication is not available"
            }
        }
    }
}

#Preview {
    ContentView()
}

代码功能

1、检查当前设备是否支持生物识别验证。

2、如果支持,则调用生物识别功能进行身份验证。

3、根据验证结果(成功或失败),执行不同的逻辑。

4、如果设备不支持生物识别,则返回错误信息。

代码解析

这段 Swift 代码实现了设备生物识别验证(如 Face ID 或 Touch ID)的逻辑,主要是用来检查设备是否支持生物识别验证,并根据用户的验证结果执行相应操作。以下是详细解析:

1、创建生物识别上下文

let context = LAContext()
var error: NSError?

LAContext 是 LocalAuthentication 框架中的类,用来管理身份验证的上下文环境。

error 是一个可选的 NSError,用于存储设备不支持生物识别验证时的错误信息。

2、检查设备是否支持生物识别

if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)

canEvaluatePolicy 是用来检查设备是否支持特定的身份验证策略。

参数 .deviceOwnerAuthenticationWithBiometrics 表示使用生物识别(如 Face ID 或 Touch ID)验证用户身份。

如果设备支持生物识别功能,canEvaluatePolicy 返回 true;否则返回 false 并将错误信息存储在 error 中。

3、调用生物识别验证

context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
    DispatchQueue.main.async {
        if success {
            isAuthenticated = true
            errorMessage = ""
        } else {
            isAuthenticated = false
            errorMessage = authenticationError?.localizedDescription ?? "Unknown error"
        }
    }
}

1)evaluatePolicy 方法

发起生物识别验证,弹出系统提供的身份验证界面。

参数

.deviceOwnerAuthenticationWithBiometrics:生物识别验证策略。

localizedReason:向用户解释为什么需要验证(会显示在系统弹出的验证对话框中)。

2)闭包返回两个参数

success:一个布尔值,表示验证是否成功。

authenticationError:验证失败时的错误信息。

3)DispatchQueue.main.async

确保 UI 更新在主线程中完成。

4)成功时的逻辑

isAuthenticated = true
errorMessage = ""

5)失败时的逻辑

isAuthenticated = false
errorMessage = authenticationError?.localizedDescription ?? "Unknown error"

4、处理设备不支持生物识别的情况

DispatchQueue.main.async {
    errorMessage = error?.localizedDescription ?? "Biometric authentication is not available"
}

如果设备不支持生物识别,进入 else 块。

更新 errorMessage,告知用户设备不支持生物识别验证,并提供错误的具体描述。

生物识别认证报错

在首次识别时,可能存在无法识别认证的情况,Xcode报错输出:

This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSFaceIDUsageDescription key with a string value explaining to the user how the app uses this data.

这是因为应用没有在Info.plist文件中声明使用Face ID的权限描述,因此iOS系统拒绝了对生物识别功能的访问。这是一个必要的步骤,否则应用会崩溃。

解决步骤

1、找到项目的Info.plist文件。

2、在Info.plist文件中新增

Privacy - Face ID Usage Description

我们需要使用 Face ID 来解锁您的数据。 // Value值

Privacy – Face ID Usage Description 是一个键(Key),它代表应用请求使用 Face ID 功能时显示的权限描述。

右侧的 Value 列是键的对应值,也就是应用向用户解释为什么需要使用 Face ID 的具体内容。

3、重新启动应用,问题得到解决。

用户控制生物识别认证

1、添加控制生物识别的布尔值

@AppStorage("isBiometricEnabled") private var isBiometricEnabled = false

2、给生物识别添加判断,检查是否启用识别变量

// 检查用户是否启用了生物识别验证
guard isBiometricEnabled else {
    print("Biometric authentication is disabled by the user.")
    return
}

3、添加生物识别控制按钮

Toggle("Enable Biometric Authentication", isOn: $isBiometricEnabled)
.padding()

修改后,实现用户通过按钮控制生物识别认证功能。

实现效果

完整代码示例

import SwiftUI
import LocalAuthentication

struct ContentView: View {
    @State private var isUnlocked = false
    @AppStorage("isBiometricEnabled") private var isBiometricEnabled = false

    func authenticate() {
        let context = LAContext()
        var error: NSError?

        // 检查用户是否启用了生物识别验证
        guard isBiometricEnabled else {
            print("Biometric authentication is disabled by the user.")
            return
        }

        // 检查是否可以使用生物识别
        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            let reason = "We need to unlock your data."

            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
                DispatchQueue.main.async {
                    if success {
                        self.isUnlocked = true
                    } else {
                        print("Authentication failed: \(authenticationError?.localizedDescription ?? "Unknown error")")
                    }
                }
            }
        } else {
            print("Biometric authentication is not available: \(error?.localizedDescription ?? "Unknown error")")
        }
    }

    var body: some View {
        NavigationView {
            VStack {
                if isUnlocked {
                    Text("Unlocked")
                        .font(.largeTitle)
                        .foregroundColor(.green)
                } else {
                    Text("Locked")
                        .font(.largeTitle)
                        .foregroundColor(.red)
                }

                Toggle("Enable Biometric Authentication", isOn: $isBiometricEnabled)
                    .padding()

                Button("Authenticate") {
                    authenticate()
                }
                .padding()
            }
            .navigationTitle("Biometric Settings")
        }
    }
}

#Preview {
    ContentView()
}

生物识别验证的备选方案

如果设备不支持生物识别或者用户拒绝验证,可以提供密码作为备选方案:

context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)

deviceOwnerAuthentication 的含义

.deviceOwnerAuthentication 是一种更通用的身份验证策略。

它不仅支持生物识别验证(如 Face ID 或 Touch ID),还支持设备密码(PIN、图案或设备解锁密码)作为备选方案。

如果设备不支持生物识别,或者用户多次失败,系统会自动回退到设备密码验证。

如何使用

可以将 .deviceOwnerAuthentication 添加到现有代码中,作为生物识别验证的备选方案。例如:

func authenticate() {
    let context = LAContext()
    var error: NSError?

    // 检查设备是否支持生物识别或设备密码验证
    if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
        let reason = "Authenticate to access your account"

        context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authenticationError in
            DispatchQueue.main.async {
                if success {
                    isAuthenticated = true
                    errorMessage = ""
                } else {
                    isAuthenticated = false
                    errorMessage = authenticationError?.localizedDescription ?? "Unknown error"
                }
            }
        }
    } else {
        // 设备不支持生物识别或密码验证
        DispatchQueue.main.async {
            errorMessage = error?.localizedDescription ?? "Authentication is not available"
        }
    }
}

1、替换 deviceOwnerAuthenticationWithBiometrics

将策略从 .deviceOwnerAuthenticationWithBiometrics 替换为 .deviceOwnerAuthentication:

如果用户设备支持生物识别,会优先使用生物识别。

如果用户多次失败,或者设备不支持生物识别,系统会回退到密码验证。

2、用户体验改进

用户在生物识别验证失败后,可以直接输入密码继续验证,而无需额外处理逻辑。

即使用户的设备完全不支持生物识别(例如旧设备),仍然可以通过密码验证。

注意事项

1、Face ID/Touch ID 描述不再适用

在弹出的验证对话框中,localizedReason 会展示给用户。如果用户用的是密码验证,这个描述可能需要更通用的描述,例如:

进行身份验证以安全访问您的帐户

2、错误处理

在用户取消或失败后,可以检查 authenticationError,并根据错误类型提供具体提示。例如:

LAError.userCancel:用户取消验证。

LAError.authenticationFailed:身份验证失败。

3、回退逻辑

如果用户设备完全不支持 .deviceOwnerAuthentication(非常少见),可以提示用户使用另一种方法访问应用(例如注册账号或输入固定密码)。

实际应用

在实际的应用中,将 authenticate 方法封装到单独的 Swift 文件中是更好的选择,尤其是在以下情况下:

1、代码复用性

如果应用需要在多个地方使用生物识别验证(例如登录、支付或解锁某些功能),将认证逻辑封装到一个单独的文件中可以避免代码重复,提高复用性。

2、代码可读性

将生物识别相关的代码与视图逻辑分离,可以让 ContentView 保持简洁,专注于界面布局和交互,而认证逻辑则集中在一个专门的文件中,便于维护和扩展。

3、测试和维护

单独的文件更容易进行单元测试,并且在未来需要修改认证逻辑时,改动的范围更集中。

1、创建认证服务文件

创建一个新的 Swift 文件,例如 BiometricAuth.swift:

import LocalAuthentication

struct BiometricAuth {
    
    static let shared = BiometricAuth()
    
    private init() {}
    func authenticate(reason: String, completion: @escaping (Bool, String?) -> Void) {
        let context = LAContext()
        var error: NSError?

        // 检查设备是否支持生物识别或设备密码验证
        if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {

            context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authenticationError in
                DispatchQueue.main.async {
                    if success {
                        completion(true,nil)
                    } else {
                        let message = authenticationError?.localizedDescription ?? "Authentication failed"
                        completion(false, message)
                    }
                }
            }
        } else {
            // 设备不支持生物识别或密码验证
            DispatchQueue.main.async {
                    let message = error?.localizedDescription ?? "Biometric authentication is not available"
                    completion(false, message)
            }
        }
    }
}

authenticate 方法

reason: 传入用于显示在 Face ID 或 Touch ID 提示框中的说明。

completion: 回调函数,用于返回认证结果(成功或失败)和错误信息。

2、在视图中使用封装方法

在 ContentView 中调用封装的 BiometricAuth 服务:

struct ContentView: View {
    @State private var isAuthenticated = false  // false表示未通过人脸识别
    @State private var isErrorMessage = false    // 人脸识别错误信息,true表示显示
    @State private var showHomeView = false // 进入首页
    @AppStorage("isBiometricEnabled") var isBiometricEnabled = false   // true表示启用密码保护
    
    // 密码保护方法
    func authenticate() {
        // 检查用户是否启用了生物识别验证
        guard isBiometricEnabled else {
            print("Biometric authentication is disabled by the user.")
            return
        }
        BiometricAuth.shared.authenticate(reason: "Authenticate to access your account") { success, error in
            if success {
                isAuthenticated = true
                isErrorMessage = false
            } else {
                isAuthenticated = false
                isErrorMessage = true
            }
        }
    }

    var body: some View {
        if isBiometricEnabled && !isAuthenticated && !showHomeView {
        // 当设置人脸识别,并且人脸识别为false时显示验证视图
            BiometricAuthView(isAuthenticated: $isAuthenticated,isErrorMessage:$isErrorMessage) {
                authenticate()
            }
        } else {
            // 如果人脸通过,显示主界面内容
            NavigationStack {
                // 主界面内容
            }
        }
    }
}

3、生物识别视图

import SwiftUI

struct BiometricAuthView: View {
    @Binding var isAuthenticated: Bool
    @Binding var isErrorMessage: Bool
    var onAuthenticate: () -> Void
    // 鸣谢页面
    var body: some View {
        VStack {
            Spacer()
            Button(action: {
                onAuthenticate() // 调用闭包执行认证
            }, label: {
                HStack {
                    Image(systemName: "faceid")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 24, height: 24) // 设置具体大小
                    Text("Unlock")
                }
            })
            if isErrorMessage {
                Text("Unlock failed, please try again.")
            }
            Spacer()
        }
    }
}

4、应用逻辑梳理

创建了一个BiometricAuth文件用于封装生物识别代码,通过单例模式方便全局调用。

在视图中使用一个封装方法:

// 密码保护方法
func authenticate() {
    // 检查用户是否启用了生物识别验证
    guard isBiometricEnabled else {
        print("Biometric authentication is disabled by the user.")
        return
    }
    BiometricAuth.shared.authenticate(reason: "Authenticate to access your account") { success, error in
        if success {
            isAuthenticated = true
            isErrorMessage = false
        } else {
            isAuthenticated = false
            isErrorMessage = true
        }
    }
}

这个方法的作用是,当用户通过按钮或其他方式触发authenticate方法后,首先判断定义的isBiometricEnabled(是否启用密码保护),这是一个UserDefaults属性。如果用户启用了密码保护,那么isBiometricEnabled就变成true。

如果没有启用,则guard会直接退出该方法。

guard isBiometricEnabled else {}

如果启用了密码保护(isBiometricEnabled为true),那么authenticate方法会通过BiometricAuth的单例模式,执行BiometricAuth内部的authenticate方法。

authenticate方法传递了一个固定的reason字符串,并传递了一个success布尔值和error字符串。

当返回的success为true时,表示验证成功。isAuthenticated赋值为true,表示通过人脸识别。isErrorMessage赋值为false,表示不显示错误信息。否则,验证不通过,显示错误信息。

在视图中的逻辑为,如果开启了密码保护,未通过人脸识别,未显示主视图,那么显示验证视图。

if isBiometricEnabled && !isAuthenticated && !showHomeView {
    // 当设置人脸识别,并且人脸识别为false时显示验证视图
        BiometricAuthView(isAuthenticated: $isAuthenticated,isErrorMessage:$isErrorMessage) {
            authenticate()
        }
    } else {
        // 如果人脸通过,显示主界面内容
        ...
    }
}

前面两个可以理解,开启密码保护,未通过人脸识别,那么就应该显示验证视图。第三个显示主视图的需求主要在于当应用开启人脸识别后,不立即显示验证视图。

当在视图中,默认情况下密码保护可能是未开启的状态,人脸识别也是未启用的状态,这就导致,当启用密码保护后,视图会立即弹出验证视图,影响到用户体验,其次是如果你的启用密码保护是一个Sheet页,当点击按钮启用密码保护时,根据前面的判断会自己弹出验证视图。当验证完成后,原先的Sheet页可能会被弹出的验证视图顶替,就导致无法再打开Sheet页,因为原先的Sheet页没有关闭掉,但也没有显示,而识别弹出的视图以其他的方法顶替。因此,就会卡在无法打开Sheet页的情况。

解决方案就是,设置一个进入主视图的变量showHomeView。如果已经进入到主视图,那么可能是已经解锁或者未启用密码保护的状态。

if isBiometricEnabled && !isAuthenticated && !showHomeView {
    // 当设置人脸识别,并且人脸识别为false时显示验证视图
        BiometricAuthView(isAuthenticated: $isAuthenticated,isErrorMessage:$isErrorMessage) {
            authenticate()
        }
    } else {
        // 如果人脸通过,显示主界面内容
    NavigationStack {
    }
    .onAppear {
        showHomeView = true
    }
}

在设置视图中,启用密码保护后,人脸识别条件会判定是否显示验证视图:

if isBiometricEnabled && !isAuthenticated && !showHomeView

这里的isBiometricEnabled(密码保护)为true,!isAuthenticated(是否通过人脸识别,默认为false)也变成了true,那么应该会立即弹出验证视图,但是当校验showHomeView变量时,因为已经进入过主视图,那么!showHomeView就变成了false。因此不会在开启密码保护后,马上弹出验证中视图。

而验证视图的部分则是通过Binding绑定了主视图的两个变量。如果验证视图通过验证,绑定的变量发生改变,完成验证。

总结

本文中的密码保护、人脸识别、生物识别实际上都指的是,通过LocalAuthentication完成的校验功能。

通过LocalAuthentication可以在实际的应用中,丰富应用的功能,保护应用的安全隐私。但实际上如果密码保护是通过UserDefault存储的话,实际上并不会真正的保护隐私数据,只是应用识别的一个障眼法。

参考资料

Using Touch ID and Face ID with SwiftUI:https://www.hackingwithswift.com/books/ios-swiftui/using-touch-id-and-face-id-with-swiftui

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注