[Swift] FirebaseAuthenticationを使用したユーザー登録機能メモ

## はじめに

iOSアプリ開発の学習で、FirebaseAuthenticationを使用してログイン機能を実装しました。

実装した機能の一部抜粋ではありますが、備忘録的に残しておきます。

## この記事に書いたこと

- createUserメソッドを使用したユーザー登録機能
- signInメソッドを使用したユーザーログイン機能
- sendEmailVerificationメソッドを使用した確認メール送信機能

※Firebaseの導入については本記事では触れません。

## 実装

### UserAuthManager

ユーザーの認証を受け持つクラスを作成します。
外部サービスのimportをあちこちに記述するのはあまりよろしくないので、
`import FirebaseAuth`等の記述はこのファイルのみで完結できるように設計します。

```UserAuthManager.swift
import Foundation
import FirebaseAuth
import Firebase

final class UserAuthManager {
    static let shared: UserAuthManager = .init()
    private init(){}

// ~~~~~ 処理を実装 ~~~~~ 
}
```

### UserAuthError(エラー型の用意)

サーバーからエラーが返ってくるケースに備えて作成しておきます。

```UserAuthManager.swift
enum UserAuthError: Error, LocalizedError {
    
    case createUserError(errorMessage: String)
    case userLoginError(errorMessage: String)
    case sendEmailVerificationError(errorMessage: String)
    case checkEmailVerification(errorMessage: String)
    
    var errorDescription: String? {
        switch self {
        case .createUserError(let message):
            return "ユーザー作成エラー：\(message)"
        case .userLoginError(let message):
            return "ログインエラー：\(message)"
        case .sendEmailVerificationError(let message):
            return "確認メール送信エラー：\(message)"
        case .checkEmailVerification(let message):
            return "メールアドレス検証エラー：\(message)"
        }
    }
}
```

### createUser(ユーザーの新規作成)

```UserAuthManager.swift
func createUser(email: String, password: String, completion: @escaping (Result<Bool, UserAuthError>) -> Void) {
    Auth.auth().createUser(withEmail: email, password: password) { authResult, error in
        
        if let error = error {
            completion(.failure(UserAuthError.createUserError(errorMessage: error.localizedDescription)))
            return
        }
        
        guard authResult != nil else {
            completion(.failure(UserAuthError.createUserError(errorMessage: "入力された情報では登録できませんでした")))
            return
        }
        completion(.success(true))
    }
}
```

#### Result
クロージャーで`Result<Bool, UserAuthError>`を返していますが、これはユーザー登録出来たかどうか以外特に渡したいパラメータも無かったので成功時Bool型にしています。

ただ失敗時は基本的にError型が返されるのでfalseが使われることがないのはちょっと気になっているところです。
もっと良い書き方がありそう。



#### error

サーバーから返ってきた結果について何パターンか処理を分けます。

1. **errorが返ってきた場合(errorがnilでない)** <br> この場合はResultでUserAuthErrorを返します。この時、呼び出し元にエラーの原因(通信状況？アドレス被り？など)も知らせる必要があるので`UserAuthError.createUserError(errorMessage: error.localizedDescription)`としてサーバーから受け取ったエラーメッセージも付けて返します。
1. **errorは無いがauthResultも無い場合** <br> errorが返ってきてないのにも関わらずauthResultが無い場合は、サーバー側も想定外のエラーとなるので、アプリ側でエラーメッセージを用意してUserAuthErrorを返します。
1. **エラーもなくauthResultがある場合** <br> 登録成功しているので`.success(true)`を返します。

### signIn(ログイン)

```UserAuthManager.swift
func login(email: String, password: String, completion: @escaping (Result<Bool, UserAuthError>) -> Void) {
    Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
        
        if let error = error {
            completion(.failure(UserAuthError.userLoginError(errorMessage: error.localizedDescription)))
        }
        
        guard authResult != nil else {
            completion(.failure(UserAuthError.userLoginError(errorMessage: "入力された情報ではログインできませんでした")))
            return
        }
        completion(.success(true))
    }
}
```

やっていることはcreateUserとほぼ同じです。


### sendEmailVerification(メールアドレスの確認)

不正なメールアドレスの使用を防ぐため、登録したメールアドレスに確認用のメールを送信する機能です。
受けとったメールアドレスのリンクをタップすることで、currentUser.isEmailVerifiedというパラメータがfalseからtrueに変わります。trueであれば確認済みの状態ということになります。

```UserAuthManager.swift
func sendEmailVerification(completion: @escaping (Result<Bool, UserAuthError>) -> Void) {
    guard let user = Auth.auth().currentUser else { return }
    user.sendEmailVerification { error in
        if let error = error {
            completion(.failure(UserAuthError.sendEmailVerificationError(errorMessage: error.localizedDescription)))
        }
        else {
            completion(.success(true))
        }
    }
}

func checkEmailVerificationResult(completion: @escaping (Result<Bool, UserAuthError>) -> Void) {
    guard let user = Auth.auth().currentUser else { return }
    
    user.reload { error in
        if let error = error {
            completion(.failure(UserAuthError.checkEmailVerification(errorMessage: error.localizedDescription)))
        }
        else {
            completion(.success(user.isEmailVerified))
        }
    }
}
```

関数を２つ用意しています。

`sendEmailVerification()`はcurrentuserのメールアドレスに確認用のメールを送信するためのものです。
エラーなく送信完了の場合は`true`を返し、エラーがある場合は`UserAuthError `を返します。

`checkEmailVerificationResult()`はメールアドレスの確認が終了しているか否かをチェックするためのものです。
エラーが返された場合(チェック自体が出来なかった場合)は`UserAuthError `を返し、エラーがなければ`isEmailVerified`のBool値を返します。

#### 補足
`sendEmailVerification`はcurrentUserが持つメソッドなので、登録完了したユーザーに対して行うことになるという点に注意


### Routing(おまけ)
アプリの起動経路の道中でユーザのログイン状態によって画面遷移先を変えます。
細かいところは省略していますが、`userLoginCheck`という関数を用意して`currentuser`を取得してログイン状態のチェック、及びメール確認の有無を見ています。

```swift
// Routerなどの画面遷移処理を受け持つクラス
func showRoot() {
    UserAuthManager.shared.userLoginCheck { result in
        switch result {
        case .success(let user): // ログインしている場合
            if user.emailVerified {
                // さらにメールの確認も済んでいる場合は、ログイン後のトップページ等へ
            }
            else {
                // メールの確認が済んでいない場合は、メールの確認処理を受け持つ画面へ
            }   
        case .failure: // ログインしていない場合
            // ログイン・新規登録の画面へ
        }
    }
}
```

```UserAuthManager.swift
// UserAuthManagerクラスに関数を用意
func userLoginCheck(completion: @escaping (Result<User, UserAuthError>) -> Void) {
    let user = Auth.auth().currentUser
    
    guard let userEmail = user?.email,
          let userEmailVerified = user?.isEmailVerified else {
        return completion(.failure(UserAuthError.loginFailure))
    }
    completion(.success(User(emailAddress: userEmail, emailVerified: userEmailVerified)))
}
```


## 終わりに

 このようにユーザー認証用のクラスを用意しておくことで、Viewから`UserAuthManager.shared.hogehoge { クロージャー }`という形で呼び出せます。
FirebaseAuthは個人アプリ開発などでもお世話になると思いますので、今後さらに理解を深めていければと思います。