自分はSmartNewsやGunosyのようなメディアアプリを作成する際に、FirebaseのRealtime Databaseを最初使っていましたが、途中からCloud Firestoreへ移行しました。
移行前のコードと移行後のコードを書いておきます。
Realtime Databaseでの記事情報の取得
まずは記事情報を取得するためのモデルを書きます。
DataSnapshot型はFirebaseからデータを取得する際の、Realtime Database特有のデータ型です。
Realtime Databaseから、DataSnapshot型のデータを取得させたら、それをこのモデル(ArticleData.swift)を経由させます。
ArticleData.swiftの中で、DataSnapshot型を別途UITableViewCellなどで再利用可能な汎用的な型に変えていくという流れです。
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する関数を記述していきます。
/*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を利用時
//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での記事情報の取得
先と同じく、まずはモデルを書きます。
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には差分の情報が入ります。
/*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利用時と一緒なので、前半は省略します。
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の差分データ取得
【FireBase】 FireStoreで簡単なぼっちSNSアプリを作ってみました
Firestoreを試してみた //こちらのサイトのリスナーを設定するのパートから参考になります。