iOS における生体認証

iOS では FaceID と TouchID の二つの生体認証がパスコード認証とは別に利用できる。

今回はその生体認証をアプリに組み込む方法について学ぶ。まず前提として、パスコードを含めた認証システムを使うにはimport LocalAuthenticationを読み込む必要がある。

認証のプロセス

  • 生体認証が可能かどうかチェックする
    • ここでパスコード認証を許可するかどうかを設定できる
  • 可能であれば生体認証を行なう
    • または生体認証をキャンセルしてパスコード認証を行なう

パスコード認証を許可するかどうかのフラグが何故あるかというと、iPhone5 以前のデバイスでは TouchID や FaceID が使用不可であり、そもそもそれ以降のデバイスでも生体認証を登録していないユーザがいるためである。

というわけで、全通りパターン分けをするとこのようになる。

func biometricsAuth() {
    let context = LAContext()
    let reason = "This app uses Touch ID / Face ID to secure your data."
    var authError: NSError?
    if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) {
        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in
            if success {
                // 生体認証が成功した場合
            } else {
                // 生体認証が失敗した場合
            }
        }
    } else {
        // 生体認証ができない場合
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

指紋認証のためのボタンは SF symbols で定義されているtouchidというやつが使える。

前回のコードとくっつける

パスコード入力画面に追加して動作チェックをしてみる。

import SwiftUI
import LocalAuthentication
struct PasscodeView: View {
    typealias CompletionHandler = (Result<Bool, Error>) -> Void
    let completionHandler: CompletionHandler
    let passcode: Int
    init(passcode: Int, completionHandler: @escaping CompletionHandler) {
        self.completionHandler = completionHandler
        self.passcode = passcode
    }
    var body: some View {
        GeometryReader { geometry in
            LazyVGrid(columns: Array(repeating: .init(.flexible(minimum: 60, maximum: 80), spacing: 0), count: 3), alignment: .center, spacing: 10, pinnedViews: []) {
                ForEach(Range(1...9)) { number in
                    Button(action: { addSign(sender: number)}, label: { Text("\(number)").frame(width: 60, height: 60, alignment: .center) })
                        .overlay(Circle().stroke(Color.blue, lineWidth: 1))
                }
                .buttonStyle(CircleButtonStyle())
                Button(action: { biometricsAuth() }, label: { Image(systemName: "touchid").resizable().frame(width: 40, height: 40, alignment: .center) })
                Button(action: { addSign(sender: 0) }, label: { Text("0").frame(width: 60, height: 60, alignment: .center) })
                    .buttonStyle(CircleButtonStyle())
                Button(action: {}, label: { Text("Delete").frame(width: 60, height: 60, alignment: .center) })
            }
            // 認証画面を真ん中に表示
            .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
        }
        .edgesIgnoringSafeArea(.all)
        .background(Color.white)
    }
    func addSign(sender: Int) {
        if sender == passcode {
            completionHandler(.success(true))
        } else {
            completionHandler(.success(false))
        }
    }
    func biometricsAuth() {
        let context = LAContext()
        let reason = "This app uses Touch ID / Face ID to secure your data."
        var authError: NSError?
        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) {
            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in
                if success {
                    // 生体認証が成功した場合
                    print("SUCCESS")
                } else {
                    // 生体認証が失敗した場合
                    print("FAILURE")
                }
            }
        } else {
            // 生体認証ができない場合
            print("FAILURE")
        }
    }
}
struct CircleButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .foregroundColor(configuration.isPressed ? Color.white : Color.blue)
            .overlay(Circle().stroke(Color.blue, lineWidth: 1))
            .contentShape(Circle())
            .background(Circle().foregroundColor(configuration.isPressed ? Color.blue : Color.clear))
    }
}
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