【iOS】Firebaseど素人のFirebase入門 Firestore編

<a hrf="https://qiita.com/awesome_hiraoka/items/3fd75fb698e4db621439">【iOS】Firebaseど素人のFirebase入門 認証編</a>に続き、今回はFirestoreを学びながらサンプルアプリ作りに挑戦してみました。

##作ったサンプルアプリのデモ
![イメージ.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678954/7551bd44-5027-9109-3068-d3e4293477f4.gif)

Firestore上のデータがTableView上に表示されるアプリです。

・`Set Button`を押すと、Firestoreに情報が書き込み or 更新（setdata）されます。
・`Add Button`を押すと、Firestoreに情報が追加（addDocument）されます。
・`Firestoreからドキュメントを取得を押す`を押すと、Firestoreに保存されている情報を取得（getDocuments）して、TableViewに反映します。

・`リアルタイムアップデート`スイッチをOnにすると、Firestore上の情報が更新される度に自動的にTextViewを更新します。

##まずはFirebaseに登録
1.Firebase公式ドキュメントに沿って、Firebase登録とFirebase SDKをアプリに追加する。
<a href="https://firebase.google.com/docs/ios/setup?hl=ja">Firebase を iOS プロジェクトに追加する</a>

2.Firebaseに追加したアプリのコンソールからCloud Firestoreを使用できるように設定する

##AppDelegateでFirestoreの初期化
AppDelegate.swiftに<b>Firebase</b>をインポート

```swift:AppDelegate.swift
import Firebase
```
`didFinishLaunchingWithOptions`内で<b>Firestore</b>の初期化

```swift:AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        FirebaseApp.configure()
        let db = Firestore.firestore()
        print(db)
        
        return true
    }
```

##ViewControllerにもFirebaseをインポート
作成を進めるファイルにもFirebaseをインポートする。

```swift:FirestoreViewController.swift
import Firebase
```

##データの書き込み or 更新
データの書き込み or 更新には`setData(documentData: [String : Any], completion:)`を使用します。

```swift:
db.collection("users").document("HiroshiTachi").setData([
                "name": name,
                "age": age,
            ]) { error in
                if let error = error {
                    print("ドキュメントの書き込みに失敗しました:", error)
                } else {
                    print("ドキュメントの書き込みに成功しました")
                }
            }
```
`setData`での書き込みの場合は、ドキュメント内に何もデータがない場合は新規作成を行い、すでにデータがある場合はそのデータの中身を更新します。

##データの追加
データの追加には`addDocument(data: , completion: )`を使用します。

```swift:
var ref: DocumentReference? = nil
ref = db.collection("users").addDocument(data:[
                "name": name,
                "age": age,
            ]) { error in
                if let error = error {
                    print("ドキュメントの追加に失敗しました:", error)
                } else {
                    print("ドキュメントの追加に成功しました:", ref?.documentID as Any)
                }
            }
```
こちらは`setData`とは違い、指定した`collection`内に問答無用でどんどん新規追加してくれます。
なので、データを更新したりするのには使用出来ません。

##データを１回取得
データを１回取得するには、`getDocuments()`を使用します。

```swift:
db.collection("users").getDocuments() { (querySnapshot, error) in
                if let error = error {
                    print("ドキュメントの取得に失敗しました:", error)
                } else {
                    print("ドキュメントの取得に成功しました")
                    for document in querySnapshot!.documents {
                        let data = document.data()
                        //取得したデータに対しての処理を書く
                        print(data["name"])
                        }
                    }
                }
            }
```

##データをリアルタイムで取得する
データが更新される度にリアルタイム取得をするには、`addSnapshotListener(listener:)`を使用します。

###1.グローバル変数としてリスナー`ListenerRegistration`を定義

```swift:
var listener: ListenerRegistration?
```

###2.リスナーをアタッチ
```swift
listener = db.collection("users").addSnapshotListener { documentSnapshot, error in
                if let error = error {
                    print("ドキュメントの取得に失敗しました", error)
                } else {
                    self.queriedDataArray = []
                    if let documentSnapshots = documentSnapshot?.documents {
                        for document in documentSnapshots {
                            let data = document.data()
                            //アップデートされた際に行いたい処理を書く
                            print(data["name"])
                                }
                            }
                        }
                    }
                }
            }
```

###3.リスナーを切り離し
リスナーは必要がなくなったら切り離し（デタッチ）しましょう。

```swift
listener?.remove()
```
公式ドキュメントより
>データをリッスンする必要がなくなったら、イベント コールバックが呼び出されないようにリスナーをデタッチしなければなりません。これにより、クライアントは更新を受信するための帯域幅の使用を停止できます。

##まとめ
公式ドキュメントの情報は本当に充実しているので、この記事より遥かに充実して間違いのない情報が得られますので、参考にする際は是非とも公式ドキュメントを参考にしていただければと思います。笑

また、セキュリティルールについてはまだまだなので知識を深めていきたい。

何か間違いがありましたら、優しく訂正していただけると幸いです🙇‍♂️

##参考
<a href="https://firebase.google.com/docs/firestore/quickstart?hl=ja">Cloud Firestore を使ってみる</a>
<a href="https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja">Cloud Firestore にデータを追加する</a>
<a href="https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja">Cloud Firestore でデータを取得する</a>
<a href="https://firebase.google.com/docs/firestore/query-data/listen?hl=ja">Cloud Firestore でリアルタイム アップデートを入手する</a>

##サンプルアプリのコード全体
```swift:FirestoreViewController.swift
import UIKit
import Firebase

class FirestoreViewController: UIViewController {
    
    @IBOutlet weak var queryDataTableView: UITableView!
    
    @IBOutlet weak var setTextLabel: UILabel!
    @IBOutlet weak var setAgeTextField: UITextField!
    @IBOutlet weak var setButton: UIButton!
    
    @IBOutlet weak var addTextField: UITextField!
    @IBOutlet weak var addAgeTextField: UITextField!
    @IBOutlet weak var addButton: UIButton!
    
    @IBOutlet weak var getButton: UIButton!
    @IBOutlet weak var realTimeQuerySwitch: UISwitch!
    
    let db = Firestore.firestore()
    var queriedDataArray = [String()]
    
    var listener: ListenerRegistration?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        queryDataTableView.dataSource = self
        addButton.layer.cornerRadius = 15
        setButton.layer.cornerRadius = 15
        getButton.layer.cornerRadius = 15
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        listener?.remove()
    }
    
    @IBAction func didTapButton(_ sender: UIButton) {
        
        switch sender.currentTitle {
        
        //SetButton Action
        case "Set":
            
            guard let name = setTextLabel.text, let age = setAgeTextField.text else { return }
            db.collection("users").document("HiroshiTachi").setData([
                "name": name,
                "age": age,
            ]) { error in
                if let error = error {
                    print("ドキュメントの書き込みに失敗しました:", error)
                } else {
                    print("ドキュメントの書き込みに成功しました")
                }
            }
            
        //AddButton Action
        case "Add":
            
            guard let name = addTextField.text, let age = addAgeTextField.text else { return }
            
            var ref: DocumentReference? = nil
            ref = db.collection("users").addDocument(data:[
                "name": name,
                "age": age,
            ]) { error in
                if let error = error {
                    print("ドキュメントの追加に失敗しました:", error)
                } else {
                    print("ドキュメントの追加に成功しました:", ref?.documentID as Any)
                }
            }
            
        //GetButton Action
        default:
            queriedDataArray = []
            db.collection("users").getDocuments() { (querySnapshot, error) in
                if let error = error {
                    print("ドキュメントの取得に失敗しました:", error)
                } else {
                    for document in querySnapshot!.documents {
                        let data = document.data()
                        guard let name = data["name"] as? String, let age = data["age"] as? String else {
                            return
                        }
                        let nameAndAge = name + "　" + age + "歳"
                        self.queriedDataArray.append(nameAndAge)
                        DispatchQueue.main.async {
                            self.queryDataTableView.reloadData()
                        }
                    }
                }
            }
        }
    }
    
    @IBAction func didChangeRealTimeQueryState(_ sender: UISwitch) {
        
        if sender.isOn {
            print("リアルタイムアップデートOn")
            listener = db.collection("users").addSnapshotListener { documentSnapshot, error in
                if let error = error {
                    print("ドキュメントの取得に失敗しました", error)
                } else {
                    self.queriedDataArray = []
                    if let documentSnapshots = documentSnapshot?.documents {
                        for document in documentSnapshots {
                            let data = document.data()
                            if let name = data["name"] as? String, let age = data["age"] as? String {
                                let nameAndAge = name + "　" + age + "歳"
                                self.queriedDataArray.append(nameAndAge)
                                DispatchQueue.main.async {
                                    self.queryDataTableView.reloadData()
                                }
                            }
                        }
                    }
                }
            }
        } else {
            print("リアルタイムアップデートOff")
            listener?.remove()
        }
    }
}

extension FirestoreViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return queriedDataArray.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = queryDataTableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = queriedDataArray[indexPath.row]
        return cell
    }
}
```