【SwiftUI】Face ID対応のロック画面を実装してみた（iPhoneのロック画面風）

# 1. はじめに
初めまして、たなぼうと申します。
記事を書いている現在は大学生であり、独学でiosアプリを開発をしています。
（※独学のためコードが美しくない可能性高）

今回のテーマは
**・iPhoneのロック画面解除のような機能をアプリに搭載したい！**
どうやれば良いのか少し悩んだので、共有する記事になります。

全コードは以下に載せてます。
https://github.com/RyotaLab/faceID.git

# 目次
[1.完成図](#1-完成図)
[2.手順紹介](#2-手順紹介)
[3.実装（コード付き）](#3-実装)
[4.おわりに](#4-おわりに)


# 1. 完成図
![gif-faceid.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692824/fcf81e23-7c1a-d265-da87-8183f060e4e9.gif)

解除方法は
* 4つのボタンで解除
* FaceIDで解除

toggleボタンをtrueでパスコード作成、falseでパスコード解除になります。
デザインは私の趣味なので、自分で好きな色や形に変更してね！

# 2. 手順紹介

1. faceIDを行うクラスや関数を作成
2. 初期画面の分岐作成
3. パスコード作成画面を実装
4. ロック画面を実装

faceIDに関しては他の有識者の記事の方が分かりやすいため、解説は省きます。
**正直、classをコピペすれば問題なく動く**ので、「より理解したい方」のみ以下の記事がおすすめです。

[LocalAuthenticationを使用して生体認証を試してみた](https://tech.nri-net.com/entry/swift_local_authentication)

今回は**ロック画面をどうやって作成するか**をメインに解説します。

# 3. 実装
それでは始めていきましょう！
（紹介するコードをコピペしていけば、完成図のようなアプリが作れます）

### 3-1. faceIDのクラス作成

生体認証を行うために、info.plistに「Privacy - Face ID Usage Description」を追加する必要があります。Valueには生体認証を使う理由を書きましょう。

![スクリーンショット 2024-05-31 14.12.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692824/b130ed53-6d19-6f60-9c8d-b129cee3213f.png)
次にファイルを作成して、faceIDを行うclassを記述します。
以下をコピペでOK！

<details><summary><b>━━━━━━━━━━━━━━━━━━┓<br>┃&nbsp;&nbsp;&nbsp;&nbsp;ソースコードを表示（折りたたみ）&nbsp;&nbsp;&nbsp;┃<br>┗━━━━━━━━━━━━━━━━━━┛</b></summary><div>

~~~swift:FaceAuth
import Foundation
import LocalAuthentication

class FaceAuth {
    
    //認証のAPIを提供するLocalAuthenticationContext
    var context: LAContext = LAContext()
    
    //認証ポップアップに表示するメッセージ
    let reason = "FaceID"
    
    //FaceIDの処理
    func auth(complation:@escaping(Bool) -> Void) {
        
        //認証できるかのチェック
        if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil){
            
            //認証始め
            context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason ){ success, error in
                
                //成功
                if success{
                    //successが分かってからクロージャを呼び出す
                    DispatchQueue.main.async {
                        complation(true)
                    }
                    
                //失敗
                }else if let laError = error as? LAError{
                    
                    switch laError.code {
                    //認証失敗
                    case .authenticationFailed:
                        complation(false)
                        break
                    //ユーザーがキャンセルボタンを押した時
                    case .userCancel:
                        complation(false)
                        break
                    //アプリを閉じて失敗
                    case .systemCancel:
                        complation(false)
                        break
                    //パスコードがセットされてない
                    case .passcodeNotSet:
                        complation(false)
                        break
                    //パスコード認証がpolicyで許可されてない場合
                    case .userFallback:
                        complation(false)
                        break
                    //指紋認証の失敗上限
                    case .touchIDNotAvailable:
                        complation(false)
                        break
                    //指紋認証が許可されてない
                    case .touchIDNotEnrolled:
                        complation(false)
                        break
                    //指紋認証が登録されてない
                    case .touchIDLockout:
                        complation(false)
                        break
                    //アプリの内部によるキャンセル
                    case .appCancel:
                        complation(false)
                        break
                    //不明なエラー
                    case .invalidContext:
                        complation(false)
                        break
                    //よくわからん
                    case .notInteractive:
                        complation(false)
                        break
                        
                    @unknown default:
                        break
                    }
                }
            }
        }else {
            //生体認証ができない場合
        }
    }
}
~~~

</div></details>

今作成したclassを参照すれば、いつでもfaceIDは使えます！

<details><summary><b>おまけ：使い方の例を表示（折りたたみ）</b></summary><div>

~~~swift
let face:FaceAuth = FaceAuth()

//この関数を実行すれば生体認証が行われる
func exec() {
    face.auth{ result in
        if result == true {
            //成功した時の処理をここに記述

        }
    }
}
~~~

</div></details>


### 3-2. 初期画面の分岐作成
もしパスコードが設定されている場合、ロック画面に移動します。

![passcheck.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692824/76e94347-c407-2a29-9f86-006f690c4cdb.png)

\@ mainが記述されているファイルを次のように変更します。

<details><summary><b>━━━━━━━━━━━━━━━━━━┓<br>┃&nbsp;&nbsp;&nbsp;&nbsp;ソースコードを表示（折りたたみ） 　 &nbsp;&nbsp;&nbsp;┃<br>┗━━━━━━━━━━━━━━━━━━┛</b></summary><div>

~~~swift:faceIDApp
import SwiftUI

@main
struct faceIDApp: App {
    //何もない時はfalse
    let SetPass = UserDefaults.standard.bool(forKey: "SetPass")
    
    var body: some Scene {
        WindowGroup {
            if SetPass{
                //パスコードあり
                LockView()
            }else{
                //パスコードなし
                ContentView()
                    .environmentObject(passCheck())
            }
        }
    }
}
~~~

</div></details>

UserDefaultsの`"SetPass"`にて、ユーザーがパスコードを作成したかを判断し、最初に開く画面を変えます。
それだけです。

また、今後使う「1回目と2回目の入力が一致しているかを確認するためのclass」も用意しておきます。

<details><summary><b>━━━━━━━━━━━━━━━━━━┓<br>┃&nbsp;&nbsp;&nbsp;&nbsp;classを表示（折りたたみ）　　　　　&nbsp;&nbsp;&nbsp;┃<br>┗━━━━━━━━━━━━━━━━━━┛</b></summary><div>

```swift:passCheck

import Foundation

class passCheck: ObservableObject {
    @Published var firstCheck:[String?] = [nil, nil, nil, nil]
    @Published var secondCheck:[String?] = [nil, nil, nil, nil]
    
    @Published var passText:String = "パスワードを忘れると復元できません\n忘れないようご注意ください"
}
```

</div></details>

### 3-3. パスコード作成画面を実装

![passcheck.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692824/69a3e8aa-0682-10aa-6357-9693894d9c09.png)

**機能**：パスコードを2回入力し、一致した時のみパスコード作成
**画面の流れ**：最初の画面(`ContentView`) → 1回目の入力画面(`SetLockView1`) →　2回目の入力画面(`SetLockView2`)

3つの画面のコードは以下の通りです。

<details><summary><b>━━━━━━━━━━━━━━━━━━┓<br>┃&nbsp;&nbsp;&nbsp;&nbsp;ContentViewを表示（折りたたみ）　&nbsp;&nbsp;&nbsp;┃<br>┗━━━━━━━━━━━━━━━━━━┛</b></summary><div>

```swift:ContentView
import SwiftUI

enum LockPath {
    case pathSub1
    case pathSub2
}

struct ContentView: View {
    @State private var navigatePath: [LockPath] = []
    //何もない時はfalse
    @State var toggle = UserDefaults.standard.bool(forKey: "SetPass")
    
    var body: some View {
        NavigationStack(path: $navigatePath) {
            List{
                //下のNavigationLinkは関係ない
                NavigationLink(destination: Text("機能なし")) {
                    Text("サンプル")
                }
                //パスコード作成 off -> onの時のみ画面遷移
                Toggle("パスコード設定",isOn: $toggle)
                    .onAppear{
                        toggle = UserDefaults.standard.bool(forKey: "SetPass")
                    }
                    .onChange(of: toggle) {
                        if toggle {
                            //パスワード設定画面へ
                            navigatePath.append(.pathSub1)
                        }else{
                            //pass解除
                            UserDefaults.standard.set(false, forKey: "SetPass")
                            UserDefaults.standard.set(false, forKey: "UseFaceID")
                        }
                    }
            }
            .navigationDestination(for: LockPath.self){ value in
                switch value {
                    
                case .pathSub1:
                    SetLockView1(path: $navigatePath)
                case .pathSub2:
                    SetLockView2(path: $navigatePath)
                }
            }
        }//navigationStack
        
    }
}
```

</div></details>

<details><summary><b>━━━━━━━━━━━━━━━━━━┓<br>┃&nbsp;&nbsp;&nbsp;&nbsp;SetLockView1を表示（折りたたみ） &nbsp;&nbsp;&nbsp;┃<br>┗━━━━━━━━━━━━━━━━━━┛</b></summary><div>

```swift:SetLockView1
import SwiftUI

struct SetLockView1: View {
    @Binding var path: [LockPath]
    
    @Environment(\.dismiss) var dismiss
    @EnvironmentObject var passcheck: passCheck
    @State var count = 0
    
    var body: some View {
        VStack{
            Spacer()
            Text("パスコードの入力")
                .font(.title3)
                .fontWeight(.bold)

            HStack{
                //黒丸
                ForEach(0..<4) { index in
                    if passcheck.firstCheck[index] == nil {
                        Image(systemName: "circle")
                            .padding()
                    }else {
                        Image(systemName: "circle.fill")
                            .padding()
                    }
                }
            }
            
            //注意事項
            Text("\(passcheck.passText)")
                .font(.footnote)
                .multilineTextAlignment(.center)
                .foregroundColor(Color.pink)
            Spacer()
            
            //入力ボタン
            HStack{
                ForEach(1..<4){ i in
                    Button{
                        inputText(number: String(i))
                    }label:{
                        Text("\(i)")
                            .font(.title)
                            .frame(width: 90, height: 45)
                            .foregroundColor(Color(.orange))
                            .cornerRadius(5)
                            .overlay(
                                RoundedRectangle(cornerRadius: 5)
                                    .stroke(Color(.orange), lineWidth: 1.0)
                            )
                    }
                }
            }
            HStack{
                ForEach(4..<7){ i in
                    Button{
                        inputText(number: String(i))
                    }label:{
                        Text("\(i)")
                            .font(.title)
                            .frame(width: 90, height: 45)
                            .foregroundColor(Color(.orange))
                            .cornerRadius(5)
                            .overlay(
                                RoundedRectangle(cornerRadius: 5)
                                    .stroke(Color(.orange), lineWidth: 1.0)
                            )
                    }
                }
            }
            HStack{
                ForEach(7..<10){ i in
                    Button{
                        inputText(number: String(i))
                    }label:{
                        Text("\(i)")
                            .font(.title)
                            .frame(width: 90, height: 45)
                            .foregroundColor(Color(.orange))
                            .cornerRadius(5)
                            .overlay(
                                RoundedRectangle(cornerRadius: 5)
                                    .stroke(Color(.orange), lineWidth: 1.0)
                            )
                    }
                }
            }
            Button{
                inputText(number: "0")
            }label:{
                Text("0")
                    .font(.title)
                    .frame(width: 90, height: 45)
                    .foregroundColor(Color(.orange))
                    .cornerRadius(5)
                    .overlay(
                        RoundedRectangle(cornerRadius: 5)
                            .stroke(Color(.orange), lineWidth: 1.0)
                    )
            }
            
            //入力ボタン終
            Spacer()
        }
        .navigationTitle("入力")
        .navigationBarBackButtonHidden()
        .toolbar {
            ToolbarItem(placement: .topBarLeading){
                Button{
                    //リストに戻り、初期化
                    passcheck.firstCheck = [nil, nil, nil, nil]
                    passcheck.passText = "パスワードを忘れると復元できません\n忘れないようご注意ください"
                    path.removeLast()
                }label:{
                    Image(systemName: "arrowshape.turn.up.backward.fill")
                }
            }
        }
    }
    //ボタンが押された時の関数
    private func inputText(number : String) {
        for (index, getText) in passcheck.firstCheck.enumerated() {
            //nilかチェック　→ 入力済みならスキップ
            //入力したらfor文を抜け出す
            if getText == nil {
                
                passcheck.firstCheck[index] = number
                //もしi.index = 3なら　画面遷移
                if index == 3 {
                    DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
                        path.append(.pathSub2)
                    }
                }
                
                break
            }
        }
    }
}
```

</div></details>

<details><summary><b>━━━━━━━━━━━━━━━━━━┓<br>┃&nbsp;&nbsp;&nbsp;&nbsp;SetLockView2を表示（折りたたみ） &nbsp;&nbsp;&nbsp;┃<br>┗━━━━━━━━━━━━━━━━━━┛</b></summary><div>

```swift:SetLockView2
import SwiftUI

struct SetLockView2: View{
    @Binding var path: [LockPath]
    
    @Environment(\.dismiss) var dismiss
    @EnvironmentObject var passcheck: passCheck
    
    @State var isShowAlert = false
    @State var passCode = ""
    
    var body: some View{
        VStack{
            Spacer()
            Text("パスコードの再入力")
                .font(.title3)
                .fontWeight(.bold)
            
            //黒丸or白丸
            HStack{
                ForEach(0..<4) { index in
                    if passcheck.secondCheck[index] == nil {
                        Image(systemName: "circle")
                            .padding()
                    }else {
                        Image(systemName: "circle.fill")
                            .padding()
                    }
                }
            }
            
            //注意事項
            Text("\(passcheck.passText)")
                .font(.footnote)
                .multilineTextAlignment(.center)
                .foregroundColor(Color.clear)
            Spacer()
            //入力ボタン
            HStack{
                ForEach(1..<4){ i in
                    Button{
                        inputText(number: String(i))
                    }label:{
                        Text("\(i)")
                            .font(.title)
                            .frame(width: 90, height: 45)
                            .foregroundColor(Color(.orange))
                            .cornerRadius(5)
                            .overlay(
                                RoundedRectangle(cornerRadius: 5)
                                    .stroke(Color(.orange), lineWidth: 1.0)
                            )
                    }
                }
            }
            HStack{
                ForEach(4..<7){ i in
                    Button{
                        inputText(number: String(i))
                    }label:{
                        Text("\(i)")
                            .font(.title)
                            .frame(width: 90, height: 45)
                            .foregroundColor(Color(.orange))
                            .cornerRadius(5)
                            .overlay(
                                RoundedRectangle(cornerRadius: 5)
                                    .stroke(Color(.orange), lineWidth: 1.0)
                            )
                    }
                }
            }
            HStack{
                ForEach(7..<10){ i in
                    Button{
                        inputText(number: String(i))
                    }label:{
                        Text("\(i)")
                            .font(.title)
                            .frame(width: 90, height: 45)
                            .foregroundColor(Color(.orange))
                            .cornerRadius(5)
                            .overlay(
                                RoundedRectangle(cornerRadius: 5)
                                    .stroke(Color(.orange), lineWidth: 1.0)
                            )
                    }
                }
            }
            Button{
                inputText(number: "0")
            }label:{
                Text("0")
                    .font(.title)
                    .frame(width: 90, height: 45)
                    .foregroundColor(Color(.orange))
                    .cornerRadius(5)
                    .overlay(
                        RoundedRectangle(cornerRadius: 5)
                            .stroke(Color(.orange), lineWidth: 1.0)
                    )
            }
            //入力ボタン終
            Spacer()
        }//VStack
        .navigationTitle("確認入力")
        .navigationBarBackButtonHidden()
        .toolbar {
            ToolbarItem(placement: .topBarLeading){
                Button{
                    //リストに戻り、初期化
                    passcheck.firstCheck = [nil, nil, nil, nil]
                    path.removeLast()
                }label:{
                    Image(systemName: "arrowshape.turn.up.backward.fill")
                }
            }
        }
        //パスコードが一致した時のアラート
        .alert(Text("FaceIdを使いますか？"), isPresented: $isShowAlert){
            //ルートへ
            Button("はい"){
                UserDefaults.standard.set(true, forKey: "UseFaceID")
                path.removeLast(path.count)
                passcheck.firstCheck = [nil, nil, nil, nil]
                passcheck.secondCheck = [nil, nil, nil, nil]
            }
            Button("いいえ"){
                UserDefaults.standard.set(false, forKey: "UseFaceID")
                path.removeLast(path.count)
                passcheck.firstCheck = [nil, nil, nil, nil]
                passcheck.secondCheck = [nil, nil, nil, nil]
            }
        }
        
    }//body
    //ボタンが押された時の関数
    private func inputText(number : String) {
        for (index, getText) in passcheck.secondCheck.enumerated() {
            //nilかチェック　→ 入力済みならスキップ
            //入力したらfor文を抜け出す
            if getText == nil {
                
                passcheck.secondCheck[index] = number
                //全入力された時
                if index == 3 {
                    if passcheck.firstCheck == passcheck.secondCheck {
                        //同じなら保存, alert（faceID)、
                        for i in 0...3 {
                            passCode = passCode + (passcheck.firstCheck[i] ?? "")

                        }
                        DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
                            UserDefaults.standard.set(true, forKey: "SetPass")
                            UserDefaults.standard.set(passCode, forKey: "password")
                            print(passCode)
                            isShowAlert = true
                        }
                        
                    }else{
                        //違うなら初期化して１つ前へ
                        DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
                            passcheck.passText = "パスワードが1回目と異なります\nもう一度行ってください"
                            passcheck.firstCheck = [nil, nil, nil, nil]
                            passcheck.secondCheck = [nil, nil, nil, nil]
                            path.removeLast()
                        }
                    }
                }
                break
            }
        }
    }
}
```

</div></details>


長々とコードがありますが、ほとんどデザイン関連なので気負わないでね！
数字ボタンを押したときに呼ばれる`inputText(number : String)`が機能の9割です。
急ぎの方はコピペでOKです。
一様ポイントを3つ解説しておきますね。

#### 3-3-1. NavigationStack(path: $~){}を使用すること

`SetLockView2`から`ContentView`に戻るとき、2画面を一気に移動する必要があります。
 これをスムーズに行うのがNavigationStack(path: $~){}です。
 画面遷移の記録がストックされていきます（なんて便利！）
 ~~~swift
 //画面遷移
navigatePath.append(~)
//1つ前に戻る
path.removeLast()
//全部戻る
path.removeLast(path.count)
 ~~~

#### 3-3-2. DispatchQueue.main.asyncAfter(deadline: .now()+0.1){}について

画面遷移をする際に、0.1秒だけ遅らせています。
理由は**入力を反映した上で、画面遷移を行いたいから**。
0.1秒遅らせないと、ユーザー視点では4つ目を入力した瞬間に遷移してしまい、4つ目を入力した感覚がありません（自分で実験すればわかります笑）

~~~swift
//画面遷移
 DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
    path.append(.~)
}
~~~

#### 3-3-3. パスコード作成時にUserDefaultsに3つ保存する

* key = "password"：パスコードに設定した数字4つ
* key = "SetPass"：パスコードが設定されているか判断
* key = "UseFaceID"：faceIDを使うか

kya名はお好みで！

### 3-4. パスコード解除画面を実装

最後にロック解除画面を作成します。
ほとんどパスコード設定画面と同じですね。

<details><summary><b>━━━━━━━━━━━━━━━━━━┓<br>┃&nbsp;&nbsp;&nbsp;&nbsp;LockViewを表示（折りたたみ ）　 　 &nbsp;&nbsp;&nbsp;┃<br>┗━━━━━━━━━━━━━━━━━━┛</b></summary><div>

~~~swift:LockView
import SwiftUI

struct LockView: View {
    
    @State var passCheck: [String?] = [nil, nil, nil, nil]

    //->trueでcontentViewを表示
    @State var isShow = false

    //設定したパスワード
    let answer = UserDefaults.standard.string(forKey: "password")

    //ficeID関連
    let face:FaceAuth = FaceAuth()
    let useFaceID = UserDefaults.standard.bool(forKey: "UseFaceID")
    
    
    var body: some View {
        ZStack{
            VStack{
                Spacer()
                Text("パスコードの入力")
                    .font(.title3)
                    .fontWeight(.bold)
                //ボタンやらもろもろ
                HStack{
                    //黒丸
                    ForEach(0..<4) { index in
                        if passCheck[index] == nil {
                            Image(systemName: "circle")
                                .padding()
                        }else {
                            Image(systemName: "circle.fill")
                                .padding()
                        }
                    }
                }
                    Spacer()
                //入力ボタン
                HStack{
                    ForEach(1..<4){ i in
                        Button{
                            inputText(number: String(i))
                        }label:{
                            Text("\(i)")
                                .font(.title)
                                .frame(width: 90, height: 45)
                                .foregroundColor(Color(.orange))
                                .cornerRadius(5)
                                .overlay(
                                    RoundedRectangle(cornerRadius: 5)
                                        .stroke(Color(.orange), lineWidth: 1.0)
                                )
                        }
                    }
                }
                HStack{
                    ForEach(4..<7){ i in
                        Button{
                            inputText(number: String(i))
                        }label:{
                            Text("\(i)")
                                .font(.title)
                                .frame(width: 90, height: 45)
                                .foregroundColor(Color(.orange))
                                .cornerRadius(5)
                                .overlay(
                                    RoundedRectangle(cornerRadius: 5)
                                        .stroke(Color(.orange), lineWidth: 1.0)
                                )
                        }
                    }
                }
                HStack{
                    ForEach(7..<10){ i in
                        Button{
                            inputText(number: String(i))
                        }label:{
                            Text("\(i)")
                                .font(.title)
                                .frame(width: 90, height: 45)
                                .foregroundColor(Color(.orange))
                                .cornerRadius(5)
                                .overlay(
                                    RoundedRectangle(cornerRadius: 5)
                                        .stroke(Color(.orange), lineWidth: 1.0)
                                )
                        }
                    }
                }
                Button{
                    inputText(number: "0")
                }label:{
                    Text("0")
                        .font(.title)
                        .frame(width: 90, height: 45)
                        .foregroundColor(Color(.orange))
                        .cornerRadius(5)
                        .overlay(
                            RoundedRectangle(cornerRadius: 5)
                                .stroke(Color(.orange), lineWidth: 1.0)
                        )
                }
                Spacer()
                //入力ボタン終
                .onAppear{
                    //faceidをするかどうか
                    if useFaceID {
                        exec()
                    }
                }
            }
            
            if isShow {
                ContentView()
                    .environmentObject(faceID.passCheck())
                    .transition(.opacity)
            }
        }
    }
    //入力関数
    private func inputText(number : String) {
        var checkAnswer = ""
        
        for (index, getText) in passCheck.enumerated() {
            //nilかチェック　→ 入力済みならスキップ
            //入力したらfor文を抜け出す
            if getText == nil {
                
                passCheck[index] = number
                if index == 3 {
                    for i in 0...3 {
                        
                        checkAnswer = checkAnswer + (passCheck[i] ?? "")
                    }
                    if checkAnswer == answer {
                        //一致
                        DispatchQueue.main.asyncAfter(deadline: .now()+0.3) {
                            withAnimation{
                                isShow.toggle()
                            }
                        }
                        
                    }else{
                        //初期化
                        passCheck = [nil, nil, nil, nil]
                    }
                }
                
                break
            }
        }
    }
    //顔認証の関数
    func exec() {
        face.auth{ result in
            //認証が成功した時の記述
            if result == true {
                passCheck = ["a", "a", "a", "a"]
                DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
                    withAnimation{
                        isShow.toggle()
                    }
                }
            }
        }
    }
}
~~~

</div></details>

機能としては、画面を開いたときにfaceID認証。もしfaceIDが「ダメor設定なし」ならボタン入力という形です。
入力後は、ZStackでContentViewを表示する形します。
2つのポイントを解説します。

#### 3-4-1. faceID関連の説明

以下の`exec()`を使用すれば、好きなタイミングでfaceID認証が行えます。
私の場合は`onAppear`の中に記述し、画面表示した際に認証を行なっています。
~~~swift
let face:FaceAuth = FaceAuth()

func exec() {
    face.auth{ result in
        if result == true {
            //認証を成功した時の記述
        }
    }
}
~~~
#### 3-4-2. withAnimation{}の必要性

ZStackで画面遷移を行なっているため、withAnimation{}がないと、画面遷移がオシャレではありません（個人の感想）。パッ！パッ！で感じで遷移してしまうんですよね。
私は`.transition(.opacity)`を使用しています。←ゆっくり透明化
（withAnimationはお好みで色々試してみてね）

# 4. おわりに
今までのコードをコピペすれば、完成図のように動きます。**プライバシーを守りたいアプリを作りたい方は参考にしてみてください！**

全コードは以下に載せてます。
https://github.com/RyotaLab/faceID.git

#### 最後に宣伝です。
このような誕生日を通知するiosアプリをリリースしています。
[誕生日book-通知とメモをリストで管理](https://apps.apple.com/jp/app/誕生日book-通知とメモをリストで管理/id6496679663)
![スクリーンショット 2024-05-31 17.00.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692824/440d9c08-0882-3678-f122-9a9ca6276fe9.png)
今回紹介したパスコード機能もあります。是非使ってみてください！