SwiftでFirebaseを用いた会員登録の実装

#はじめに
Firebaseとチャット機能の学習としてLINEのクローンアプリを作成しているのですが、その際のFirebaseを用いた会員登録の実装を備忘録として投稿します。
初学者ですので訂正点ございましたら、ご指摘よろしくお願いします。


#概要
今回、会員登録機能を実装するにあたり下記のように順序立てしました。
１．`FirebaseAuth`へユーザー登録
２．`FirebaseStorage`へプロフィール画像を登録
３．`FirebaseFirestore`へユーザー情報を登録

- `メールアドレス`、`パスワード`、`ユーザーネーム`の全てに記入がされていれば登録処理可（新規登録ボタン有効）としています。
- `１．`の処理の際に、承認メールを使い会員登録を完了させてから`２．`の処理に移るのがよくある手法かと思われますが、今回の目的が学習のためでしたのでその辺りは考慮しておりません。

#実行環境
【Xcode】Version 12.0.1
【Swift】Version 5.3
【CocoaPods】version 1.9.3
【Firebase】version 6.29.0


#実装後の画面
![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692635/d2270f5e-4a55-475a-cb0b-66050a47d62c.gif)


#実装コード
```swift:SignUpModel.swift
import Foundation
import Firebase

//delegateはweak参照したいため、classを継承する
protocol SignUpModelDelegate: class {
    func createImageToFirestorageAction()
    func createUserToFirestoreAction(fileName: String?)
    func completedRegisterUserInfoAction()
}

class SignUpModel {
    
    // delegateはメモリリークを回避するためweak参照する
    weak var delegate: SignUpModelDelegate?
    
    func createUser(email: String, password: String) {
        // FirebaseAuthへ保存
        Auth.auth().createUser(withEmail: email, password: password) { (res, err) in
            if let err = err {
                print("FirebaseAuthへの保存に失敗しました。\(err)")
                // ユーザー情報の登録が失敗した時の処理
                return
            }
            print("FirebaseAuthへの保存に成功しました。")
            // FirebaseAuthへ保存完了 -> FirebaseStorageへ保存処理
            self.delegate?.createImageToFirestorageAction()
        }
    }
    
    func creatrImage(fileName: String, uploadImage: Data) {
        // FirebaseStorageへ保存
        let storageRef = Storage.storage().reference().child("profile_image").child(fileName)
        storageRef.putData(uploadImage, metadata: nil) { (metadate, err) in
            if let err = err {
                print("Firestorageへの保存に失敗しました。\(err)")
                // ユーザー情報の登録が失敗した時の処理
                return
            }
            print("Firestorageへの保存に成功しました。")
            // FirebaseStorageへ保存完了 -> FirebaseFirestoreへ保存処理
            self.delegate?.createUserToFirestoreAction(fileName: fileName)
        }
    }
    
    func createUserInfo(uid: String, docDate: [String : Any]) {
        // FirebaseFirestoreへ保存
        Firestore.firestore().collection("users").document(uid).setData(docDate as [String : Any]) { (err) in
            if let err = err {
                print("Firestoreへの保存に失敗しました。\(err)")
                // ユーザー情報の登録が失敗した時の処理
                return
            }
            print("Firestoreへの保存に成功しました。")
            // ユーザー情報の登録が完了した時の処理
            self.delegate?.completedRegisterUserInfoAction()
        }
    }

}

```

```swift:SignUpViewController.swift
import UIKit
import Firebase
import FirebaseStorage
import IQKeyboardManagerSwift

class SignUpViewController: UIViewController {
    
    @IBOutlet weak var profileImageButton: UIButton!
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var userNameTextField: UITextField!
    @IBOutlet weak var signUpButton: UIButton!
    
    let signUpModel = SignUpModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        IQKeyboardManager.shared.enable = true
        
        emailTextField.delegate = self
        passwordTextField.delegate = self
        userNameTextField.delegate = self
        signUpModel.delegate = self
        
        // 画面UIについての処理
        setupUI()
        
    }
    
    // 画面UIについての処理
    func setupUI() {
        signUpButton.layer.cornerRadius = 3
        signUpButton.isEnabled = false
        profileImageButton.layer.masksToBounds = true
        profileImageButton.layer.cornerRadius = 75
        profileImageButton.layer.borderColor = UIColor.lightGray.cgColor
        profileImageButton.layer.borderWidth  = 0.1
    }
    
    // プロフィール画像の選択（フォトライブラリーへ遷移）
    @IBAction func profileImageButtonAction(_ sender: Any) {
        let imagePickerController = UIImagePickerController()
        imagePickerController.allowsEditing = true
        imagePickerController.delegate = self
        self.present(imagePickerController, animated: true, completion: nil)
    }
    
    // 新規登録処理
    @IBAction func signUpButtonAction(_ sender: Any) {
        
        guard let email = emailTextField.text,
              let password = passwordTextField.text
        else { return }
        
        // FirebaseAuthへ保存
        signUpModel.createUser(email: email, password: password)      
    }
    
    # ・・・省略・・・
    
    // プロフィール画像をFirebaseStorageへ保存する処理
    private func createImageToFirestorage() {
        // プロフィール画像が設定されている場合の処理
        if let image = self.profileImageButton.imageView?.image {
            let uploadImage = image.jpegData(compressionQuality: 0.5)
            let fileName = NSUUID().uuidString
            // FirebaseStorageへ保存
            signUpModel.creatrImage(fileName: fileName, uploadImage: uploadImage!) 
        } else {
            print("プロフィール画像が設定されていないため、デフォルト画像になります。")
            // User情報をFirebaseFirestoreへ保存
            self.createUserToFirestore(profileImageName: nil)
        } 
    }
    
    // User情報をFirebaseFirestoreへ保存する処理
    private func createUserToFirestore(profileImageName: String?) {
        
        guard let email = Auth.auth().currentUser?.email,
              let uid = Auth.auth().currentUser?.uid,
              let userName = self.userNameTextField.text
        else { return }
        
        // 保存内容を定義する（辞書型）
        let docData = ["email": email,
                       "userName": userName,
                       "profileImageName": profileImageName,
                       "createdAt": Timestamp()] as [String : Any?]
        
        // FirebaseFirestoreへ保存
        signUpModel.createUserInfo(uid: uid, docDate: docData as [String : Any])
    }
    
}

extension SignUpViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    // 写真が選択された時に呼ばれるメソッド
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let editedImage = info[.editedImage] as? UIImage {
            profileImageButton.setImage(editedImage.withRenderingMode(.alwaysOriginal), for: .normal)
        } else if let originalImage = info[.originalImage] as? UIImage {
            profileImageButton.setImage(originalImage.withRenderingMode(.alwaysOriginal), for: .normal)
        }
        dismiss(animated: true, completion: nil)
    }
    
}

extension SignUpViewController: UITextFieldDelegate {
    // textFieldでテキスト選択が変更された時に呼ばれるメソッド
    func textFieldDidChangeSelection(_ textField: UITextField) {
        // textFieldが空かどうかの判別するための変数(Bool型)で定義
        let emailIsEmpty = emailTextField.text?.isEmpty ?? true
        let passwordIsEmpty = passwordTextField.text?.isEmpty ?? true
        let userNameIsEmpty = userNameTextField.text?.isEmpty ?? true
        // 全てのtextFieldが記入済みの場合の処理
        if emailIsEmpty || passwordIsEmpty || userNameIsEmpty {
            signUpButton.isEnabled = false
            signUpButton.backgroundColor = UIColor.systemGray2
        } else {
            signUpButton.isEnabled = true
            signUpButton.backgroundColor = UIColor(named: "lineGreen")
        }
    }
    
    // textField以外の部分を押したときキーボードが閉じる
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }
}

extension SignUpViewController: SignUpModelDelegate {
            
    // FirebaseAuthへ保存完了 -> FirebaseStorageへ保存処理
    func createImageToFirestorageAction() {
        print("FirebaseAuthへの保存に成功しました。")
        self.createImageToFirestorage()
    }
    
    // FirebaseStorageへ保存完了 -> FirebaseFirestoreへ保存処理
    func createUserToFirestoreAction(fileName: String?) {
        print("Firestorageへの保存に成功しました。")
        self.createUserToFirestore(profileImageName: fileName)
    }
    
    // ユーザー情報の登録が完了した時の処理
    func completedRegisterUserInfoAction() {
        // ChatListViewControllerへ画面遷移
        let storyboard = UIStoryboard(name: "ChatList", bundle: nil)
        let chatListVC = storyboard.instantiateViewController(withIdentifier: "ChatListVC") as! ChatListViewController
        let nav = UINavigationController(rootViewController: chatListVC)
        nav.modalPresentationStyle = .fullScreen
        nav.modalTransitionStyle = .crossDissolve
        self.present(nav, animated: true, completion: nil)
    }
    
}

```

Firebaseに関しての処理にフォーカスを当てていますので下記項目の説明は省かせていただきます。

- エラー処理について
- `UIActivityIndicatorView`について
- `UIImagePickerController`について
- `IQKeyboardManagerSwift`について（詳細は[こちら](https://github.com/hackiftekhar/IQKeyboardManager)参照ください。）


#準備
###①前提条件
下記項目が完了しているのを前提条件としまして進めていきます。

- Firebaseプロジェクトの作成
- XcodeでのFirebaseSDK組み込み

このあたりがまだという方は、[ドキュメント](https://firebase.google.com/docs/ios/setup?hl=ja)を参照よろしくお願いします。

###②Xcode側の準備
`Podfile`に以下を追加して、ターミナルで`pod install`をします。

```
  pod 'Firebase/Analytics'
  pod 'Firebase/Auth'
  pod 'Firebase/Core'
  pod 'Firebase/Firestore'
  pod 'Firebase/Storage'
  pod 'FirebaseUI/Storage'
```

###③Firebase側の準備
![スクリーンショット 2020-10-12 20.15.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692635/55f8c357-63e4-c397-dbbe-0d15b98219a7.png)
上図のように`Sign-in method`タブから`メール/パスワード`を選択します。鉛筆のアイコンで編集画面を開きます。

![スクリーンショット 2020-10-12 20.15.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692635/26969b8c-d965-e705-e482-7dd77007a30d.png)
有効にしたら保存します。準備としてはこれで終わりになります。



#実装詳細①（FirebaseAuth編）

```swift:SignUpViewController.swift
// 新規登録処理
@IBAction func signUpButtonAction(_ sender: Any) {
        
    guard let email = emailTextField.text,
          let password = passwordTextField.text
    else { return }
        
    // FirebaseAuthへ保存
    signUpModel.createUser(email: email, password: password)      
}
```

```swift:SignUpModel.swift
func createUser(email: String, password: String) {
    // FirebaseAuthへ保存
    Auth.auth().createUser(withEmail: email, password: password) { (res, err) in
        if let err = err {
            print("FirebaseAuthへの保存に失敗しました。\(err)")
            // ユーザー情報の登録が失敗した時の処理
            return
        }
        print("FirebaseAuthへの保存に成功しました。")
        // FirebaseAuthへ保存完了 -> FirebaseStorageへ保存処理
        self.delegate?.createImageToFirestorageAction()
    }
}
```


#実装詳細②（FirebaseStorage編）

```swift:SignUpViewController.swift
// FirebaseAuthへ保存完了 -> FirebaseStorageへ保存処理
func createImageToFirestorageAction() {
    print("FirebaseAuthへの保存に成功しました。")
    self.createImageToFirestorage()
}
```

```swift:SignUpViewController.swift
// プロフィール画像をFirebaseStorageへ保存する処理
private func createImageToFirestorage() {
    // プロフィール画像が設定されている場合の処理
    if let image = self.profileImageButton.imageView?.image {
        // 画像を圧縮
        let uploadImage = image.jpegData(compressionQuality: 0.5)
        // ユニークなIDを取得
        let fileName = NSUUID().uuidString
        // FirebaseStorageへ保存
        signUpModel.creatrImage(fileName: fileName, uploadImage: uploadImage!) 
    } else {
        print("プロフィール画像が設定されていないため、デフォルト画像になります。")
        // User情報をFirebaseFirestoreへ保存
        self.createUserToFirestore(profileImageName: nil)
    } 
}
```

```swift:SignUpModel.swift
func creatrImage(fileName: String, uploadImage: Data) {
    // FirebaseStorageへ保存
    let storageRef = Storage.storage().reference().child("profile_image").child(fileName)
    storageRef.putData(uploadImage, metadata: nil) { (metadate, err) in
        if let err = err {
            print("Firestorageへの保存に失敗しました。\(err)")
            // ユーザー情報の登録が失敗した時の処理
            return
        }
        print("Firestorageへの保存に成功しました。")
        // FirebaseStorageへ保存完了 -> FirebaseFirestoreへ保存処理
        self.delegate?.createUserToFirestoreAction(fileName: fileName)
    }
}
```

#実装詳細③（FirebaseFirestore編）

```swift:SignUpViewController.swift
// FirebaseStorageへ保存完了 -> FirebaseFirestoreへ保存処理
func createUserToFirestoreAction(fileName: String?) {
    print("Firestorageへの保存に成功しました。")
    self.createUserToFirestore(profileImageName: fileName)
}
```

```swift:SignUpViewController.swift
// User情報をFirebaseFirestoreへ保存する処理
private func createUserToFirestore(profileImageName: String?) {
        
    guard let email = Auth.auth().currentUser?.email,
          let uid = Auth.auth().currentUser?.uid,
          let userName = self.userNameTextField.text
    else { return }
        
    // 保存内容を定義する（辞書型）
    let docData = ["email": email,
                   "userName": userName,
                   "profileImageName": profileImageName,
                   "createdAt": Timestamp()] as [String : Any?]
        
    // FirebaseFirestoreへ保存
    signUpModel.createUserInfo(uid: uid, docDate: docData as [String : Any])
}
```

```swift:SignUpModel.swift
func createUserInfo(uid: String, docDate: [String : Any]) {
    // FirebaseFirestoreへ保存
    Firestore.firestore().collection("users").document(uid).setData(docDate as [String : Any]) { (err) in
        if let err = err {
            print("Firestoreへの保存に失敗しました。\(err)")
            // ユーザー情報の登録が失敗した時の処理
            return
        }
        print("Firestoreへの保存に成功しました。")
        // ユーザー情報の登録が完了した時の処理
        self.delegate?.completedRegisterUserInfoAction()
    }
}
```

```swift:SignUpViewController.swift
// ユーザー情報の登録が完了した時の処理
func completedRegisterUserInfoAction() {
    // ChatListViewControllerへ画面遷移
    # ・・・省略・・・
}
```


#参考

- [【Swift】 LINE風Chatアプリ作成(Ep1) - 概要の説明とChatList画面の作成を解説](https://www.youtube.com/watch?v=XandgrGiV-8&list=PLJGQf09UDweLB6NGPoYnNL7j_z0Rzkpgr&index=1)
- [[Firebase][iOS] Firebase Authentication で会員機能を作ってみよう
](https://dev.classmethod.jp/articles/firebase-authentication-ios/)
- [【iOS】FirebaseStorageへ画像を保存・取得する方法](http://hari-blog.com/firestore-storage)