SwiftでFirestore読み書きを実装する

今回はUserというデータの読み書きを実装します
## 0. Firestoreを有効にする
Firebaseのプロジェクトを開き、`Firestore Database`を開きます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540178/f155454a-4f24-49a7-e3d5-5e3b999998fa.png)
`データベースの作成`をクリックし、`テストモードで開始`を押します
リリース時にはセキュリティルールを実装する必要がありますが、今回は最低限の実装が目的なのでテストモードを利用します

`ロケーション`はどこでも良いです。今回は`東京`を選択しました

`完了`を押し、Firestoreを有効にします


## 1. UserというStructを作成する
今回は`UserDataStore.swift`というファイルを作成しました
このswiftファイル内で、Userの定義やFirestoreとのデータのやり取りを記述します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540178/c70a85d8-ab77-c3f6-8376-a31ca11e1e3c.png)
```swift:UserDataStore.swift
import Foundation

struct User: Codable {
    let displayName: String
    let createdTime: Date?
    let uid: String
    
    enum CodingKeys: String, CodingKey {
        case displayName = "display_name"
        case createdTime = "created_time"
        case uid
    }
}
```
今回は、displayName, createdTime, uidという3つのプロパティを持つUser構造体を実装します
CodingKeysエンマーションで、JSONとの整合性を取るために、プロパティ名とJSONのキー名をマッピングしています

## 2. Firestoreとデータをやりとりする部分を実装する
```swift:UserDataStore.swift
final class UserDataStore{
    
    /// 引数で渡されたuserオブジェクトをFirestoreに保存します
    /// - Parameters:
    ///   - user: 保存するユーザー情報
    ///   - completion: 保存処理の成否を返すクロージャー
    static func createUser(user: User, completion: @escaping (_ success: Bool) -> Void) {
        let db = Firestore.firestore()
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .secondsSince1970
        let data = try! encoder.encode(user)
        var dictionary = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
        dictionary["created_time"] = dateToTimestamp(user.createdTime)
        db.collection("users").document(user.uid).setData(dictionary) { (error) in
            if let error = error {
                print(error.localizedDescription)
                completion(false)
                return
            }
            completion(true)
        }
    }
    
    /// 現在のユーザーの情報を更新します
    /// - Parameters:
    ///   - displayName: 更新後の表示名
    ///   - completion: 更新処理の成否を返すクロージャー
    static func updateUser(displayName: String, completion: @escaping (_ success: Bool) -> Void) {
        let db = Firestore.firestore()
        let userRef = db.collection("users").document(Auth.auth().currentUser!.uid)
        userRef.updateData(["display_name": displayName]) { (error) in
            if let error = error {
                print(error.localizedDescription)
                completion(false)
                return
            }
            completion(true)
        }
    }
    
    /// Firestoreから現在のユーザーの情報を取得します。
    /// - Parameters:
    ///   - completion: 取得したユーザー情報を返すクロージャー
    static func fetchUserData(completion: @escaping (_ user: User?) -> Void) {
        let db = Firestore.firestore()
        let userRef = db.collection("users").document(Auth.auth().currentUser!.uid)
        userRef.getDocument { (snapshot, error) in
            if let error = error {
                print(error.localizedDescription)
                completion(nil)
                return
            }
            guard let snapshot = snapshot, snapshot.exists else {
                completion(nil)
                return
            }
            do {
                let data = snapshot.data()
                let user = try JSONDecoder().decode(User.self, from: JSONSerialization.data(withJSONObject: data))
                completion(user)
            } catch {
                print(error)
                completion(nil)
            }
        }
    }
    
    static func dateToTimestamp(_ date: Date?) -> Timestamp? {
        guard let date = date else { return nil }
        return Timestamp(date: date)
    }
}
```

内容としては、

- createUserメソッドは、引数で渡されたuserオブジェクトをFirestoreに保存します。まず、JSONEncoderを使用して、userオブジェクトをJSONデータに変換します。その後、Firestoreに保存するために、JSONオブジェクトを辞書に変換します。

- updateUserメソッドは、現在のユーザーの情報を更新します。このメソッドは、FirestoreのAPIを使用して、現在のユーザーのドキュメントを更新します。

- fetchUserDataメソッドは、Firestoreから現在のユーザーの情報を取得します。このメソッドは、FirestoreのAPIを使用して、現在のユーザーのドキュメントを取得し、それをUserオブジェクトに変換します。

- dateToTimestampメソッドは、DateからTimestampに変換します。

です。

--- 

ちなみに、completionとは、関数が完了した時に呼び出されるクロージャー（関数の一種）のことです。
これは、関数が完了した後に実行したい処理を指定するために使用されます。 

例えば、上記のコードでは、createUser関数やfetchUserData関数は、Firestoreからデータを取得したり保存したりする処理を行っています。そして、これらの関数が完了した後に、completionクロージャーに渡された関数が呼び出されます。その関数は、引数によって指定された結果を元に、アプリケーションのUIを更新するなどの処理を行います。

## 3. 実際に使ってみる

新規会員登録時に表示名を設定する画面を実装してみましょう。

https://qiita.com/sugijotaro/items/bbd532d6e1b07bdfb669

この記事と、

https://qiita.com/sugijotaro/items/dc06cc5e70bdbe42ec48

この記事を参考に、ログイン部分まで実装しましょう。

あるいは、これらの記事のリポジトリを参考に実装しましょう

https://github.com/sugijotaro/firebase-demo

`SignUpViewController.swift`を作成し、TextFieldとボタンを設置します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540178/78cb494d-66d9-dc0b-5dfc-6fcc1a560e2d.png)
そして、以下のように実装しました。
```SignUpViewController.swift
import UIKit
import FirebaseAuth

class SignUpViewController: UIViewController {
    
    @IBOutlet var nameTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

    }
    
    @IBAction func registerButtonTapped() {
        if let name = nameTextField.text, !name.isEmpty {
            let user = User(displayName: name, createdTime: Date(), uid: Auth.auth().currentUser!.uid)
            UserDataStore.createUser(user: user) { (success) in
                if success {
                    let mainVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "HomeView") as UIViewController
                    mainVC.modalPresentationStyle = .fullScreen
                    mainVC.modalTransitionStyle = .crossDissolve
                    self.present(mainVC, animated: true)
                } else {
                    print("Failed to create user")
                }
            }
        }
    }
    
}
```
registerButtonTappedメソッド内では、
- nameTextFieldに入力された文字列を取得し、空でない場合にのみ以下の処理を実行
    - Userクラスのインスタンスを生成し、そのインスタンスには、nameTextFieldに入力された文字列、現在日時、Firebaseの認証情報から取得したユーザーIDが格納される
    - UserDataStoreクラスのcreateUserメソッドを呼び出し、生成したUserクラスのインスタンスを引数に渡す
    - createUserメソッドのコールバックによって、処理が成功した場合にはHomeViewというIdentifierを持つViewControllerへ遷移し、失敗した場合には"Failed to create user"というメッセージを出力する

という処理を行っています

詳しい実装はこちらのリポジトリをご覧ください

https://github.com/sugijotaro/firebase-demo

## 参考

https://firebase.google.com/docs/firestore?hl=ja

https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja#swift

https://puroguradesu.hatenadiary.jp/entry/2021/01/28/002531