はじめに
この記事は、Hameeアドベントカレンダー18日目の記事です。
この記事では、Webエンジニアがモバイルアプリ開発に興味を持ち、SwiftとFirebaseの勉強を兼ねて分報アプリを作る際に溜まったノウハウを書き溜めた記事です。
この記事で取り上げている分報アプリは開発途中で、全部を取り上げると非常に長ったらしくなるので今回はドメイン層の説明を中心に取り上げます。
デモ
開発途中のα版ですがデモです。
ドメイン層の実装に手こずりすぎてUIの実装全く追いつかなかったなんて言い訳はしない
Firestoreでのデータベース設計
今回はモバイルアプリケーションということで、データの蓄積などは天下のGoogle様が提供するFirestoreを使用することにしました。
Firestoreでは、Webエンジニアが慣れ親しんでいるRDMSとは違い、NoSQLデータベースです。
データモデルとして、コレクションとドキュメントとフィールドの主に3つの階層が用意されています。
詳しくはFirestoreの公式ドキュメントをご覧ください(`・ω・´)
- Users
- name:String
- Reports
- author: Reference
- content: String
今回は分報アプリということで、以下の2つのテーブルについて設計を進めます。
( ˘⊖˘) 。o (codicで「分報」って打ったらminutely_reportingって出てきたので、普通だったらposts
と命名するところをreports
としました(安直)
Usersテーブル
ユーザの情報を保存するコレクションです。
今はとりあえず名前をString型で格納しているだけです。
Reportsテーブル
投稿内容を保存するコレクションは以下の通りです。
投稿日時の実装はTimeStamp型からDate型への変換が上手くいかず時間的に間に合いませんでした
ここでのポイントは、Reportsテーブルのauthorが参照型であることです。
Firestoreでは、参照型のデータ型を使用することが出来ます。
そのため、ユーザと投稿を紐づけたい場合はReference型を使って紐づけてあげると、後々タイムラインの実装がやりやすくなります。
[もっと詳しく解説している素晴らしい記事👏👏👏]
Cloud Firestoreの勘所 パート2 — データ設計
FireStore の Document Reference Type について
投稿機能の実装
データ設計が終わったところで、実際に分報を投稿する機能を実装します。
基本的にはFirestoreの公式ドキュメントを参考にしながら実装を進めることで大概は上手くいきます。
投稿と取得の両方に関わることですが、ドキュメントを見ると
Firestore.firestore().colletcion("コレクション名").document("ドキュメント名")
のような感じでクエリの対象となる場所を指定して、
それに対してsetData
もしくはgetData
メソッドを実行するだけで良いみたいです。
とても便利です(人´∀`).☆.。.:*・゚
import Firebase
class SetReportUsecase: SetReportProtocol
{
//ここでFirestoreValueとドキュメントパスを定義
let db = Firestore.firestore()
let saveDocument = Firestore.firestore().collection("reports").document()
public func setReport() {
//setDataメソッドを使うことでFirestoreへのデータ追加が可能
saveDocument.setData([
//フィールドの指定
]) { error in
// クエリ実行後のエラーハンドリングなどをここで行う
}
}
}
ここで1つポイントがあります。
ここから先Firestoreと通信する際にはほぼ登場することになるcompletion
についてです。
モバイルアプリ側とFirestoreの処理は基本的に非同期で行われるので、アプリ側はFirestore側の処理が終わるまで待つ必要があります。
Swiftでは、そのような非同期処理をcompletion
を使って処理が終わるまで待つようにすることが出来ます。
以下のように、Firestore側のクエリ処理を待つ側のメソッドの引数にcompletion
を追加し、メソッドの終了と同時にその結果を返すように実装することが出来ます。
以下は例です。
//引数に追加
public func setReport(content: String, completion: @escaping (QuerySnapshot)->()) {
saveDocument.setData([
//ゴニョゴニョします
)] { (result, error) in
if error == nil {
// クエリの実行が終了し、エラーが無ければ呼び出し元に結果を投げる
completion(result)
}
}
また、先述したユーザテーブルへのReferenceについては、以下のように別クエリでUsersテーブルからユーザ情報を取得してそのリファレンスをデータに追加しています。
var userRef: DocumentReference!
let userRefString = db.collection("users").document("taro")
self.userRef = db.document(userRefString.path)
投稿に関する全体的なコードとしては以下の通りです。
class SetReportUsecase: SetReportProtocol
{
let db = Firestore.firestore()
let saveDocument = Firestore.firestore().collection("reports").document()
var isSuccess = true
var userRef: DocumentReference!
/**
* Set a new Report to firestore
*/
public func setReport(content: String, completion: @escaping (Bool)->()) {
self.getUserReference() { userRef in
self.userRef = userRef
}
saveDocument.setData([
"authorRef": self.userRef,
"content": content
]) { error in
if error != nil {
fatalError("\(error)")
self.isSuccess = false
completion(self.isSuccess)
}
completion(self.isSuccess)
}
}
private func getUserReference(completion: @escaping (DocumentReference)->()) {
let userRefString = db.collection("users").document("taro")
let userRef = db.document(userRefString.path)
completion(userRef)
}
}
記事取得の実装
記事の取得も、投稿と同様にcompletion
を使って公式ドキュメント通りに進めれば大概上手くいきます。それでも詰まったのが筆者で(ぁ
import Firebase
class GetReportsUsecase: GetReportProtocol {
var reports = [Report]()
let db = Firestore.firestore()
/**
* Get all reports from firestore
*/
public func getAllReports(completion: @escaping ([Report])->()) {
let reportDocRef = db.collection("reports")
reportDocRef.getDocuments() { (querySnapshot, err) in
if err == nil, let querySnapshot = querySnapshot {
for document in querySnapshot.documents {
let data = document.data()
let report = Report(author: data["author"], content: data["content"] as! String)
self.reports.append(report)
}
completion(self.reports)
} else if err != nil {
completion(self.reports)
print("\(err)")
}
}
}
}
戻り値として、FirestoreのDictionary型
をそのままUIに返すのもアリですが、別途構造体を作ってそのオブジェクトとして返却をしてあげた方が、のちのちフィールドの追加が発生した時に修正が容易ですし、何よりコードが綺麗になります。
struct Report {
var author: Any
var content: String
}
終わりに
Webエンジニアですが、モバイルアプリの世界は知らないことばかりで楽しいなぁ!と毎日心踊らせています。
今回はドメイン層の紹介がほとんどでしたが、UIの実装は大変ながらも自分のコードが即反映されているかつある程度のUIコンポーネントが揃っているので、自分の思い描いたアプリがすぐ作れて楽しいです。
もう少し作り込んだらUI版の記事も出そうと思います。
最後まで読んでくださり、ありがとうございます(`・ω・´)ゞビシッ!!