Firebase Authenticationを使ってiOSアプリのログイン機能を実装する

最近、iOSアプリにFirebase Authentication を使ってSMS認証を実装する機会がありました。
実際に利用してみた感想としては、`ログイン機能` や `新規会員登録` などの複雑を要する機能を比較的簡単に品質の高く実装できるのでおすすめ出来ると感じました。


## 前提
- Firebase/Auth をプロジェクトに追加し、 `pod install` の実行までが完了している
  - まだの方は、[Firebase を iOS プロジェクトに追加する](https://firebase.google.com/docs/ios/setup?hl=ja) を参照し**FirebaseSDKの導入と、Firebase/Authのinstall**をお願いします。
- 各イベントのバインディング処理についてはRxSwift、RxCocoaを利用する
  - RxSwiftに関する理解はこちらの記事がおすすめです: [RxSwiftについてようやく理解できてきたのでまとめることにした(1)](https://qiita.com/s_emoto/items/b000a5c076f3d6076972)

　
## 参考となる資料
Firebaseはドキュメントが充実しており、環境構築に関しては公式のドキュメントの手続きに沿えば基本的に問題ないと感じました。

- **GoogleによるFirebase公式ドキュメント**
 - [Firebase を iOS プロジェクトに追加する](https://firebase.google.com/docs/ios/setup?hl=ja)
 - [iOS で Firebase Authentication を使ってみる](https://firebase.google.com/docs/auth/ios/start?hl=ja)
 - [iOS での電話番号を使用した Firebase 認証](https://firebase.google.com/docs/auth/ios/phone-auth?hl=ja)

- **その他の参考とした資料**
  - [Firebase Authentication の基本的な使い方](https://medium.com/swift-column/firebase-authentication-b6dd43b53b5e)
  - [iOSで電話番号認証を行う方法](https://qiita.com/kyam_/items/bf3953cf7b3504c7326a)


## Firebaseへ電話番号の送信を行う
- ユーザーのiPhoneに認証コードをSMSで送信するようFirebaseにリクエストする処理は以下です
- 詳細については公式ドキュメントを参照ください → [ユーザーの電話に確認コードを送信する](https://firebase.google.com/docs/auth/ios/phone-auth?hl=ja#send-a-verification-code-to-the-users-phone)

**[注意点]**　日本の電話番号コード `+81` を電話番号の先頭に付与する必要があるので、注意してください。

```swift
func sendPhoneNumber(phoneNumber: String) {
        
    let num = "+81" + phoneNumber
    // Firebaseへ電話番号を送信
    PhoneAuthProvider.provider().verifyPhoneNumber(num, uiDelegate: nil) { [weak self] verificationID, error in
        if let error = error { // Errorの場合はこちらへ }
        if let id = id {
            // 取得したauthVerificationIDをUserDefaultsへ保存
            UserDefaults.standard.set(verificationID, forKey: "authVerificationID")
            // SMS画面へ遷移させる
        }
    }
}
```
`verificationID` を永続領域に保存するメリットについては、Firebaseの公式ドキュメントに記載がある通りです。

> 確認 ID を保存し、アプリの読み込み時に復元します。これにより、ユーザーがログインフローを完了する前にアプリが終了した場合でも（たとえば SMS アプリへの切り替え時など）、有効な確認 ID を残すことができます。


## 確認コードを使ってユーザーをログインさせる
- 詳細については公式ドキュメントを参照ください → [確認コードを使ってユーザーをログインさせる
](https://firebase.google.com/docs/auth/ios/phone-auth?hl=ja#sign-in-the-user-with-the-verification-code)

以下の関数を使ってユーザの **クレデンシャル** を発行します。
**クレデンシャル** とは、ユーザのIDやPWなどのユーザ情報におけるパラメータの総称です。
クレデンシャルを生成するための関数の第一引数には電話番号を送信する際に永続領域に保存した `verificationID ` を渡し、第二引数にはユーザが実際に入力した確認コードを渡します。

コールバックでログイン結果が返されるので、返ってきた結果によってエラー文言を出し分けたり、インジケータを表示してユーザに通信状態を伝えるとより良いと思いました。

```swift
// ユーザから入力された確認コードで
func sendSMSAuthCode(code: String) {
    // クレデンシャルの発行
    let id = userDefault.object(forKey: "authVerificationID") as? String ?? ""
    let credential = PhoneAuthProvider.provider().credential(withVerificationID: id, verificationCode: code)

    Auth.auth().signIn(with: credential) { [weak self] authResult, error in
        if let error = error {
            guard let errorCode = AuthErrorCode(rawValue: error._code) else { return }
            switch errorCode {
            case .invalidVerificationCode: print("認証コードが正しくありません")
            default: print("エラーが起きました。しばらくしてから再度お試しください。")
            }
        }
        if let _ = authResult {
            let currentUser = Auth.auth().currentUser
            currentUser?.getIDTokenForcingRefresh(true) { idToken, _ in
                guard let _ = idToken else {
                    print("トークンの取得に失敗しました")
                    return
                }
                // LOGIN SUCCESS!!!!
            }
        }
    }
}
```

以上で、ログイン機能は完了で🎉🎉


## アプリ初回起動時にログイン状態にあるかどうかを取得したい
ログイン機能は実装できましたが、アプリ起動時にユーザにログインフローを毎回踏んで貰うのは現実的ではありません。
Firebaseは特にユーザに操作をさせず、ログイン済みかどうかのステータスを取得する事ができます。したがって、AppDelegateやユーザ情報を取得する処理の中で以下の処理を実行するとログイン済みの場合はtokenが取得出来ます。
私はとりあえず、 `tokenの取得成功` `サインインが必要` `エラー` のステータスを用意してみました。

```swift
enum FirebaseTokenCallbackResult { case success(token: String), needSignIn, error(error: Error?) }

func fetchFirebaseToken() -> Observable<FirebaseTokenCallbackResult> {
    return Observable.create { observer in
        guard let currentUser = Auth.auth().currentUser else {
            // 自分自身のアカウントが取得できなかった場合は、新規会員登録のフローを踏む
            observer.onNext(.needSignIn)
            return Disposables.create()
        }
        currentUser.getIDToken(completion: { [weak self] idToken, error in
           if let token = idToken {
                self?.firebaseToken = token
                return observer.onNext(.success(token: token))
            }
            return observer.onNext(.error(error: error))
        })
        return Disposables.create()
    }
}
```

#### 注意点
AppDelegateで以下の処理を呼び出して初期化すると思いますが、以下の処理が実行されるよりも先にAuthなどの処理を呼び出してしまうとクラッシュする可能性があります。そのため、以下の処理が実行されるよりも先に `currentUser` などを取得しないよう注意が必要です。

```swift
FirebaseApp.configure()
```

以上です。