データのfetchingにおけるRealtime DatabaseとCloud Firestoreの比較

自分はSmartNewsやGunosyのようなメディアアプリを作成する際に、FirebaseのRealtime Databaseを最初使っていましたが、途中からCloud Firestoreへ移行しました。
移行前のコードと移行後のコードを書いておきます。

# Realtime Databaseでの記事情報の取得

まずは記事情報を取得するためのモデルを書きます。
DataSnapshot型はFirebaseからデータを取得する際の、Realtime Database特有のデータ型です。
Realtime Databaseから、DataSnapshot型のデータを取得させたら、それをこのモデル（ArticleData.swift）を経由させます。
ArticleData.swiftの中で、DataSnapshot型を別途<font color=green>UITableViewCell</font>などで再利用可能な汎用的な型に変えていくという流れです。

```ArticleData.swift
import UIKit
import Firebase
import FirebaseDatabase


class ArticleData: NSObject {

    var id: String?
    var articleUrl:String?
    var genreName:String?
    var sourceName:String?
    var summary:String?
    var imageUrl:String?
    var imageString: String?
    var titleStr: String?
    var likes: [String] = []
    var isLiked: Bool = false
    var clipSumNumber:Int?
    var relatedArticleIDs:[String] = []
    
    //snapshotにはRealtime Databaseから取ってきたDataSnapshot型の記事情報が入る。
    //myIdには同様にUseのIDが入る。このIDは"いいね"数を計算する際に使用する。
    init(snapshot: DataSnapshot, myId: String) {
        self.id = snapshot.key
        
        let valueDictionary = snapshot.value as! [String: AnyObject]
        
        //以下keyに対応させて引っ張ってきている
        self.articleUrl = valueDictionary["articleUrl"] as? String
        self.genreName = valueDictionary["genreName"] as? String
        self.imageUrl = valueDictionary["imageUrl"] as? String
        self.sourceName = valueDictionary["sourceName"] as? String
        self.titleStr = valueDictionary["titleStr"] as? String
        self.summary = valueDictionary["summary"] as? String

        if let relatedArticleIDs = valueDictionary["relatedArticleIDs"] as? [String]{
            self.relatedArticleIDs = relatedArticleIDs
        }
        
        //Realtime Databaseのkey"likes"には、配列形式で記事に"いいね"したユーザーのIDが格納されている。それを[String]型にキャストして取り出している。
        if let likes = valueDictionary["likes"] as? [String] {
            self.likes = likes
        }
        
        for likeId in self.likes {
            //配列likesのなかに自分のユーザーIDがあれば、"いいね"済み=trueということ。
            if likeId == myId {
                self.isLiked = true
                break
            }
        }
        //"いいね"数の計算
        self.clipSumNumber = likes.count
    }
}
```

続いて、記事情報をfetchする関数を記述していきます。


```ViewController.swift

/*class宣言の中*/

var articleArray:[ArticleData] = []

func fetchArticleData() {
    articleArray = [] //呼ぶ際に空にする。これがないとfetchArticleData()が呼ばれるたびにarticleArrayが倍増していくことになる。
    if Auth.auth().currentUser != nil { //Firebase Authでログインしているかどうかをチェック
        let articlesRef = Database.database().reference().child("ArticleData")//"ArticleDataはFirebaseのパス"
        articlesRef.observe(.childAdded, with: {snapshot in 
            if let uid = Auth.auth().currentUser?.uid {
                let articleData = ArticleData(snapshot: snapshot, myId: uid)
                if self.articleArray.count < 14 { //記事数を14記事までにする
                    self.articleArray.insert(articleData, at: 0)
                }

                self.tableView.reloadData() //これがないと、肝心の記事がload...表示されない。
                SVProgressHUD.dismiss() //SVProgressHUDについての説明は省略
            }
        })
        
        //.childChandedで、いいねボタンタップ時の挙動をキャッチしている。
        articlesRef.observe(.childChanged, with: { snapshot in
            if let uid = Auth.auth().currentUser?.uid {
                let articleData = ArticleData(snapshot: snapshot, myId: uid)
                        
                var index: Int = 0 //変更があった記事のindexを入れるため
                for article in self.articleArray { //articleArrayには上の.childAddedで引っ張ってきた記事が配列として入っている。
                    if article.id == articleData.id { //変更があった記事のidと、.childAddedで引っ張ってきた記事のidが同じだった場合のindexを取得しようとしている。
                        index = self.articleArray.index(of: article)!
                        break
                    }
                }

                //差し替えるために一度削除
                self.articleArray.remove(at: index)
                //削除したところに更新済みのデータを追加
                self.articleArray.insert(articleData, at:index)
                        
                SVProgressHUD.dismiss()
            }
        })        
    }
}
```

いいねボタンを押した時の挙動を書くコードにも変更点があるので載せておきます。
まずはRealtime Databaseを利用時

```ViewController.swift
//TableViewのデリゲートメソッド:cellForRowAt。TableViewCellの外観を定義します。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: R.nib.listCell, for:indexPath)  else { return UITableViewCell()}
    cell.setCellInfo(articleData: articleDataArray[indexPath.row])

    //このclipButtonは作成したCellの内容を定義したxib/nibファイルで、Outletで宣言したUIButtonです。このclipButtonに.addTargetを用いてhandleButtonメソッドを付加しています。
    cell.clipButton.addTarget(self, action: #selector(handleButton(sender:event:)), for:   UIControl.Event.touchUpInside)
    return cell
}


@objc func handleButton(sender:UIButton, event:UIEvent) {
    // タップされたセルのインデックスを求める
    let touch = event.allTouches?.first
    let point = touch!.location(in: self.tableView)
    let indexPath = tableView.indexPathForRow(at: point)
    
    // 配列からタップされたインデックスのデータを取り出す
    let articleData = articleArray[indexPath!.row]
    
    // Firebaseに保存するデータの準備
    if let uid = Auth.auth().currentUser?.uid {
        if articleData.isLiked {
            // すでにいいねをしていた場合はいいねを解除するためIDを取り除く
            var index = -1
            for likeId in articleData.likes {
                if likeId == uid {
                    // 削除するためにインデックスを保持しておく
                    index = articleData.likes.index(of: likeId)!
                    break
                }
            }
            articleData.likes.remove(at: index)
            
        } else {
            articleData.likes.append(uid)    
        }

        // 増えたlikesをFirebaseに保存する
        let articleRef = Database.database().reference().child("ArticleData").child(articleData.id!)
        let likes = ["likes": articleData.likes]
        articleRef.updateChildValues(likes)
    }    
}

```




# Cloud Firestoreでの記事情報の取得

先と同じく、まずはモデルを書きます。


```ArticleQueryData.swift
class ArticleData: NSObject {
    
    var id: String?
    var articleUrl:String?
    var genreName:String?
    var sourceName:String?
    var summary:String?
    var imageUrl:String?
    var imageString: String?
    var titleStr: String?
    var date: NSDate?
    var likes: [String] = []
    var isLiked: Bool = false
    var clipSumNumber:Int?
    var tags: String?
    var relatedArticleIDs:[String] = []
    
    var isCommented: Bool = false
    var commenterIDs:[String] = []
    
    init(snapshot: QueryDocumentSnapshot, myId: String) {
        self.id = snapshot.documentID

        let valueDictionary = snapshot.data() 
        
        //以下keyに対応させて引っ張ってきている
        self.articleUrl = valueDictionary["articleUrl"] as? String
        self.date = valueDictionary["date"] as? NSDate
        self.genreName = valueDictionary["genreName"] as? String
        self.imageUrl = valueDictionary["imageUrl"] as? String
        self.sourceName = valueDictionary["sourceName"] as? String
        self.titleStr = valueDictionary["titleStr"] as? String
        self.summary = valueDictionary["summary"] as? String
        self.tags = valueDictionary["tags"] as? String         
        
        if let relatedArticleIDs = valueDictionary["relatedArticleIDs"] as? [String]{
            self.relatedArticleIDs = relatedArticleIDs
        }
        
        if let likes = valueDictionary["likes"] as? [String] {
            self.likes = likes
        }
        
        for likeId in self.likes {
            if likeId == myId {
                self.isLiked = true
                break
            }
        }
        self.clipSumNumber = likes.count
    }
}

```

基本的にモデルに関しては、Realtime Databaseと変更点はあまりありません。


続いてFirestoreが記事情報をfetchしていきます。
一度取得した情報を、その後の変更点もキャッチし続けるためには、
.getDocuments() { querySnapshot }ではなく、.addSnapshotListener { querySnapshot }を使います。
~~そして、その中で、.documentChanges.forEach{ diff }を使用するのがポイントです。
diffには差分の情報が入ります。~~

```ViewController.swift

/*class宣言の中*/

var articleDataArray:[ArticleQueryData] = []

func fetchArticleData() {
    //ログイン済み（uidを取得済み）であることを確認
    if let user = Auth.auth().currentUser {
        let ref = Firestore.firestore().collection("articleData")
        let uid = user.uid
        
        ref.addSnapshotListener { querySnapshot, err in
            if let err = err {
                print("Error fetching documents: \(err)")
            } else {
                self.articleDataArray = [] //これがないと、変更のたびに記事量が倍になっていく
                for document in querySnapshot!.documents {
                    let articleData = ArticleQueryData(snapshot: document, myId: uid)
                    
                    if self.articleDataArray.count < 14 { //記事数を14記事までにする
                        self.articleDataArray.insert(articleData, at: 0)
                    }
                    self.tableView.reloadData()
                    SVProgressHUD.dismiss()
                }
                /*
                querySnapshot!.documentChanges.forEach{diff in
                    if diff.type == .added {
                        print("added: \(diff.document.data())")
                    } else if diff.type == .modified {
                        print("modified: \(diff.document.data())")                        
                    } else if diff.type == .removed {
                        print("removed: \(diff.document.data())")
                    }
                }*/
            }
        }
    }
}

```
diff.type == .addedは、初回ロード時も呼ばれるようです。
diff.type == .modifiedは、"いいね"ボタンを押して実際にFirestoreの情報を編集した時に呼ばれるようです。
diff.type == .removedは、まだ試していないです。documentの内容の削除ではなく、document自体を削除する際に呼ばれるのでは？と推測しています。

.documentChanges.forEachが必要かと思っていたら、なんかこれを使わなくても、更新をキャッチするみたいですね。差分を再利用したい時に用いるのかもしれません><
詳しい方は教えてください。


続いてFirestore利用時のいいねの挙動です。最後の部分以外はRealtime Database利用時と一緒なので、前半は省略します。

```ViewController.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //省略
}


@objc func handleButton(sender:UIButton, event:UIEvent) {
    //省略
    if let uid = Auth.auth().currentUser?.uid {
    //省略

        // 増えたlikesをFirebaseに保存する
        let articleRef = Firestore.firestore().collection("articleData").document(articleData.id!)
        let likes = ["likes": articleData.likes]

        articleRef.updateData(likes){ err in
            if let err = err {
                print("Error adding document: \(err)")
            } else {
                print("Document successfully written!")
            }
        }
    }    
}

```

[firestoreの差分データ取得](https://teratail.com/questions/151233)
[【FireBase】 FireStoreで簡単なぼっちSNSアプリを作ってみました](https://note.mu/s_kadota/n/n3f1f3e61e672)
[Firestoreを試してみた](https://qiita.com/miyae/items/6988c1b61b76b4938ae6#2リスナーを設定する)  <font color="#088A68">//こちらのサイトのリスナーを設定するのパートから参考になります。
</font>