LoginSignup
10

More than 3 years have passed since last update.

Swift+FirestoreでSNSライクなアプリを作る(Domain篇)

Last updated at Posted at 2019-12-17

はじめに

この記事は、Hameeアドベントカレンダー18日目の記事です。

この記事では、Webエンジニアがモバイルアプリ開発に興味を持ち、SwiftとFirebaseの勉強を兼ねて分報アプリを作る際に溜まったノウハウを書き溜めた記事です。

この記事で取り上げている分報アプリは開発途中で、全部を取り上げると非常に長ったらしくなるので今回はドメイン層の説明を中心に取り上げます。

デモ

開発途中のα版ですがデモです。
ドメイン層の実装に手こずりすぎてUIの実装全く追いつかなかったなんて言い訳はしない

times_someone.gif

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型で格納しているだけです。

スクリーンショット 2019-12-16 21.45.55.png

Reportsテーブル

投稿内容を保存するコレクションは以下の通りです。
投稿日時の実装はTimeStamp型からDate型への変換が上手くいかず時間的に間に合いませんでした

スクリーンショット 2019-12-16 21.45.49.png

ここでのポイントは、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版の記事も出そうと思います。
最後まで読んでくださり、ありがとうございます(`・ω・´)ゞビシッ!!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10