【iOS】Firebase Authentication + Firebase RealTime DataBase でログイン機能を実装する

## 環境
- Xcode 12.3 (12C33)
- Firebase/Auth 7.4.0
- Firebase/Database 7.4.0

## 作成するアプリ
- 社用メールアドレスでログイン後出退勤を記録できるアプリ

## 今回実現する機能
- メールアドレスとパスワードでログイン
- ログイン成功した場合にはアカウントに紐づく以下の情報を返却する
    - 姓
    - 名
    - 従業員識別キー（出退勤記録に用いる）
- ログインできるメールアドレスとパスワードはあらかじめ発行しておく
    - アプリ等からの新規アカウント登録は行わない

## 手順
1. Firebase プロジェクトを作成
2. アプリを登録
3. GoogleService-Info.plist をDLしプロジェクトに追加
4. Firebase/Auth および Firebase/Database を Pod で導入
5. Firebase のコンソール画面から Authentication を開きメールアドレスによるログインを有効化
6. 社員のメールアドレスを登録
7. json で Realtime Database にインポートするファイルを作成
8. Firebase のコンソール画面から Realtime Database を開き作成しておいたファイルをインポート
9. Realtime Database のルールの設定を変更しログインしていないアプリからデータを読み込めないようにする
10. アプリ側でサインイン機能を実装する
11. アプリ側でデータ取得機能を実装する

1〜4は本題ではないため以下記事を参照

- [Firebaseの設定をしてみた](https://qiita.com/kumas/items/fc10569bbd4130ab2eff)
- [iOSアプリにFirebaseの設定を追加してみた](https://qiita.com/kumas/items/74d74a87b04d8614259a)

## 5. Firebase のコンソール画面から Authentication を開きメールアドレスによるログインを有効化
- Firebase のコンソール画面から対象となるプロジェクトを選択
- 左側のメニューから「Authentication」を選択する
- Sign-in method タブを選択する
- メール/パスワードを選択する
- 「有効にする」をONにする

![qiita_auth_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1045667/d2bf3b74-b00c-d73a-731a-06d0e008a64b.png)


## 6. 社員のメールアドレスを登録する
- Firebase のコンソール画面の左側のメニューから「Authentication」を選択する
- Users タブを選択する
- 「ユーザーを追加」ボタンをクリックする
- メールアドレス・パスワードを記入し「ユーザーを追加」ボタンをクリックする
    - 後ほどユーザーUIDを利用する
![qiita_auth_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1045667/a1914fd1-7278-94e4-29c0-f93a6fb1c645.png)



## 7. json で Realtime Database にインポートするファイルを作成
- 以下の様に json ファイルを作成

```json:従業員データ
{
  "users" : {
    "（ユーザーUID1）" : {
      "last_name" : "テスト",
      "first_name" : "太郎",
      "key" : "(従業員識別キー1)"
    },
    "（ユーザーUID2）" : {
      "last_name" : "試験",
      "first_name" : "花子",
      "key" : "(従業員識別キー2)"
    }
  }
}
```

- ユーザーUID には Authentication > Users で確認できるユーザーUIDを利用する

## 8. Firebase のコンソール画面から Realtime Database を開き作成しておいたファイルをインポート
- Firebase のコンソール画面の左側のメニューから「Realtime Database」を選択する
- 「データ」タブを選択する
- タブ以下の画面右上の点が3つ縦に並んでいるボタンをタップする
- 「JSONをインポート」を選択する
- 7で作成したJSONファイルを選択する
- 「インポート」を選択する
- 画面に「"users"」から始まるツリー構造が追加されていることを確認する

![qiita_auth_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1045667/fad49430-77c5-0cd2-06b5-922dc4ae9a39.png)


## 9. Realtime Database のルールの設定を変更しログインしていないアプリからデータを読み込めないようにする
- Firebase のコンソール画面の左側のメニューから「Realtime Database」を選択する
- 「ルール」タブを選択する
- 「ルールを編集」が選択されていなければ選択する
- 以下の通り編集する

```json:ルール
{
  "rules": {
    ".read": false,
  	".write": false,
    "users" : {
    	"$uid": {
          ".read": "auth.token != null && $uid === auth.uid"
    	}
    }
  }
}
```
- 編集後権限の設定状況に問題がなければ「公開」を選択する

![qiita_auth_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1045667/930eac72-e2b2-e5cf-1814-c6c721311565.png)


### ルールの設定について
- ルールはカスケード式に適用される
    - 上流で true としていた場合設定されない限りは下流は全て true になる
    - 下流で false に設定されるとそこから下流は全て false になる
- 今回は全体としては外部からの読み書きを禁止する
- usersのうちの `$uid` データについて以下のルールを適用する
    - 認証トークンが存在し、かつ `$uid` （データのキー） が認証情報のユーザーUIDと一致するデータについては読み込み権限を付与する
- `$uid` は単純に変数名なので `$a` などでもOK
- `auth` からログイン後の認証情報にアクセス可能
- 詳細は以下より
    - [リアルタイムデータベースルールの使用条件](https://firebase.google.com/docs/database/security/rules-conditions?hl=ja)

## 10. アプリ側でサインイン機能を実装する

### AppDelegate
- アプリ起動時に Firebase と接続する

```swift:AppDelegate.swift

import Firebase

import UIKit


// MARK: - AppDelegate

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        // Firebase に接続する
        FirebaseApp.configure()
        
        return true
    }

    // 以下略
```

### サインイン画面
- メールアドレスとパスワードをUIから取得し結果を completion で処理する
- サインインに成功した場合 completion に返却される `AuthDataResult?` に ユーザーUID が含まれているため、これを用いてRealtime Database からデータを取得する

```swift:SignInViewController.swift

import FirebaseAuth

import UIKit


// MARK: - SignInViewController

final class SignInViewController: UIViewController {

    // 中略

    @IBAction private dynamic func signInButtonTapped(_ sender: Any) {

        guard
            let email = self.emailTextField?.text,
            let password = self.passwordTextField?.text
        else {

              return
        }

        Auth.auth().signIn(withEmail: email, password: password) { (result: AuthDataResult?, _) in
                
            if let userId = result?.user.uid {
                
                // サインイン成功時の処理
            }
            else {
                
                // サインイン失敗時の処理
            }
        }
    }

    // 以下略

```


## 11. アプリ側でデータ取得機能を実装する

### 構造体の準備
- 取得したデータを入れる構造体を作成する

```swift:FBEmployee.swift
import FirebaseDatabase

import Foundation


// MARK: - FBEmployee

struct FBEmployee {
    
    // 姓
    let lastName: String
    // 名
    let firstName: String
    // 従業員識別キー
    let employeeKey: String
    
    init(by snapshot: FirebaseDatabase.DataSnapshot) {
        
        guard
            let dictionary = snapshot.value as? NSDictionary,
            let lastName = dictionary["last_name"] as? String,
            let firstName = dictionary["first_name"] as? String,
            let employeeKey = dictionary["key"] as? String,
        else {
            
            fatalError()
        }
        
        self.lastName = lastName
        self.firstName = firstName
        self.employeeKey = employeeKey
    }
}
```

### データの取得
- Realtime Database はツリー構造になっている
    - Realtime Database へのリファレンスを作成し、そこから `child` ノードを探索するイメージでアクセス
- ユーザーUIDがデータのキーとなるように Realtime Database を作成したため、Realtime Database の `child` に `userId` を入れてアクセスする
- 今回取得するデータはほぼ変化することがなく、ログイン時に一度取得できれば良いため `observeSingleEvent(of:with:withCancel:)` を用いる
    - `observe(_:with:withCancel)` を用いる場合、利用しなくなったリスナーのデタッチを行う必要があるため注意


```swift:SignInViewController.swift

import FirebaseAuth

import FirebaseDatabase

import UIKit


// MARK: - SignInViewController

final class SignInViewController: UIViewController {

    // 中略

    @IBAction private dynamic func signInButtonTapped(_ sender: Any) {

        guard
            let email = self.emailTextField?.text,
            let password = self.passwordTextField?.text
        else {

              return
        }

        Auth.auth().signIn(withEmail: email, password: password) { (result: AuthDataResult?, _) in
                
            if let userId = result?.user.uid {
                
                // サインイン成功時の処理
                let reference = FirebaseDatabase.Database.database().reference()
            
                reference.child("users").child(userId).observeSingleEvent(of: .value) { (snapshot: DataSnapshot) in
                
                    // データ取得成功時の処理
                    // 従業員データのデコード処理
                    let employee = FBEmployee(by: snapshot)
                    // ここから画面遷移等を実行する
                
                } withCancel: { _ in
                
                    // データ取得失敗時の処理
                }
            }
            else {
                
                // サインイン失敗時の処理
            }
        }
    }

    // 以下略
```

## 参考
- [Firebaseの設定をしてみた](https://qiita.com/kumas/items/fc10569bbd4130ab2eff)
- [iOSアプリにFirebaseの設定を追加してみた](https://qiita.com/kumas/items/74d74a87b04d8614259a)
- [初めてのFirebaseを触ってみる(Database)](https://qiita.com/watataku8911/items/ac040f4671c0f9a62bd4)

### Firebase 公式リファレンス
- [iOS でパスワード ベースのアカウントを使用して Firebase 認証を行う](https://firebase.google.com/docs/auth/ios/password-auth?hl=ja)
- [リアルタイムデータベースルールの使用条件](https://firebase.google.com/docs/database/security/rules-conditions?hl=ja)
- [iOS でのデータの読み取りと書き込み](https://firebase.google.com/docs/database/ios/read-and-write?hl=ja)