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

# はじめに

この記事は、[Hameeアドベントカレンダー](https://qiita.com/advent-calendar/2019/hamee)18日目の記事です。

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

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

# デモ

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

![times_someone.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264906/52bf50eb-8b71-4424-b1eb-e519e99afc24.gif)


## Firestoreでのデータベース設計

今回はモバイルアプリケーションということで、データの蓄積などは天下のGoogle様が提供する[Firestore](https://firebase.google.com/docs/firestore?hl=ja)を使用することにしました。
Firestoreでは、Webエンジニアが慣れ親しんでいるRDMSとは違い、NoSQLデータベースです。
データモデルとして、**コレクション**と**ドキュメント**と**フィールド**の主に３つの階層が用意されています。
詳しくは[Firestoreの公式ドキュメント](https://firebase.google.com/docs/firestore/data-model?hl=ja)をご覧ください(｀･ω･´)

<dl>
  <dt>Users</dt>
  <dd>name:String</dd>
  <dt>Reports</dt>
  <dd>author: Reference</dd>
  <dd>content: String</dd>
</dl>

今回は分報アプリということで、以下の２つのテーブルについて設計を進めます。
( ˘⊖˘) 。o ([codic](https://codic.jp/engine)で「分報」って打ったら**minutely_reporting**って出てきたので、普通だったら`posts`と命名するところを`reports`としました(安直)

### Usersテーブル

ユーザの情報を保存するコレクションです。
今はとりあえず名前をString型で格納しているだけです。

<img width="689" alt="スクリーンショット 2019-12-16 21.45.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264906/497e6560-8a5d-532b-e905-0d6a81b44f20.png">


### Reportsテーブル

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

<img width="722" alt="スクリーンショット 2019-12-16 21.45.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264906/508e3d4a-0474-5e2c-df07-7560eb71ade4.png">

ここでのポイントは、**Reportsテーブルのauthorが参照型**であることです。
Firestoreでは、参照型のデータ型を使用することが出来ます。
そのため、ユーザと投稿を紐づけたい場合はReference型を使って紐づけてあげると、後々タイムラインの実装がやりやすくなります。

[もっと詳しく解説している素晴らしい記事👏👏👏]
[Cloud Firestoreの勘所 パート2 — データ設計](https://medium.com/google-cloud-jp/firestore2-920ac799345c)
[FireStore の Document Reference Type について](https://qiita.com/syamaoka/items/fac7211f5e558f49bb7a)

## 投稿機能の実装

データ設計が終わったところで、実際に分報を投稿する機能を実装します。
基本的には[Firestoreの公式ドキュメント](https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja)を参考にしながら実装を進めることで大概は上手くいきます。

投稿と取得の両方に関わることですが、ドキュメントを見ると
`Firestore.firestore().colletcion("コレクション名").document("ドキュメント名")`のような感じでクエリの対象となる場所を指定して、
それに対して`setData`もしくは`getData`メソッドを実行するだけで良いみたいです。
とても便利です(人´∀｀)．☆．。．:*･ﾟ

```swift
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
            // クエリ実行後のエラーハンドリングなどをここで行う
        }
    }
}
```

ここで１つポイントがあります。
ここから先Firestoreと通信する際にはほぼ登場することになる`completion`についてです。

モバイルアプリ側とFirestoreの処理は基本的に非同期で行われるので、アプリ側はFirestore側の処理が終わるまで待つ必要があります。

Swiftでは、そのような非同期処理を`completion`を使って処理が終わるまで待つようにすることが出来ます。
以下のように、Firestore側のクエリ処理を待つ側のメソッドの引数に`completion`を追加し、メソッドの終了と同時にその結果を返すように実装することが出来ます。
以下は例です。

```swift
//引数に追加
public func setReport(content: String, completion: @escaping (QuerySnapshot)->()) {
    saveDocument.setData([
        //ゴニョゴニョします
    )] { (result, error) in
    if error == nil {
        // クエリの実行が終了し、エラーが無ければ呼び出し元に結果を投げる
        completion(result)
    }
}
```

また、先述したユーザテーブルへのReferenceについては、以下のように別クエリでUsersテーブルからユーザ情報を取得してそのリファレンスをデータに追加しています。

```swift
var userRef: DocumentReference!

let userRefString = db.collection("users").document("taro")
self.userRef = db.document(userRefString.path)
```

投稿に関する全体的なコードとしては以下の通りです。


```swift
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`を使って[公式ドキュメント](https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja)通りに進めれば大概上手くいきます。~~それでも詰まったのが筆者で(ぁ~~

```swift
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に返すのもアリですが、別途構造体を作ってそのオブジェクトとして返却をしてあげた方が、のちのちフィールドの追加が発生した時に修正が容易ですし、何よりコードが綺麗になります。

```swift
struct Report {
    var author: Any
    var content: String
}
```


# 終わりに

Webエンジニアですが、モバイルアプリの世界は知らないことばかりで楽しいなぁ！と毎日心踊らせています。
今回はドメイン層の紹介がほとんどでしたが、UIの実装は大変ながらも自分のコードが即反映されているかつある程度のUIコンポーネントが揃っているので、自分の思い描いたアプリがすぐ作れて楽しいです。
もう少し作り込んだらUI版の記事も出そうと思います。
最後まで読んでくださり、ありがとうございます(｀･ω･´)ゞﾋﾞｼｯ!!