[SwiftUI]初めてのCloud Firestore[SPM]

※最新の情報は社内テックブログにて書き直しましたので以下参考にして下さい。
https://tech.nri-net.com/entry/firestore_swift_async_await
## 記事の内容
[Firebase Authenticationで認証機能を実装する方法](https://qiita.com/yuujioka/items/d3b310e0a3fa2ddfab5a)に引き続き、Firestoreについてもまとめてみました。
ベースはSPMでの導入後を想定した記事になっています。内容としては以下の通りです。

・Cloud Firestoreとは
・データの追加
・データの取得
・データの更新
・データの削除

この記事を読む前にFirebaseへの登録やFirebase Authenticationを利用して認証機能を実装する方法はざっと知っておく必要があります。
認証機能については[Firebase Authenticationで認証機能を実装する方法](https://qiita.com/yuujioka/items/d3b310e0a3fa2ddfab5a)にSPMで実装する方法を記載しましたので参考にしていただけると幸いです。

## 環境
・ macOS: Monterey
・ Xcode: 13.1
・ SDK Version: iOS 8.10.0

## Cloud Firestoreとは
公式ドキュメントには以下のように概要が書かれています。
>Cloud Firestore は、Firebase と Google Cloud からのモバイル、ウェブ、サーバー開発に対応した、柔軟でスケーラブルなデータベースです。Firebase Realtime Database と同様に、リアルタイム リスナーを介してクライアント アプリ間でデータを同期し、モバイルとウェブのオフラインサポートを提供します。これにより、ネットワークレイテンシやインターネット接続に関係なく機能するレスポンシブアプリを構築できます。Cloud Firestore は、その他の Firebase および Google Cloud プロダクト（Cloud Functions など）とのシームレスな統合も実現します。　※[公式ドキュメント](https://firebase.google.com/docs/firestore?hl=ja)


Firebaseでデータベースを作成でき、リアルタイムにデータを同期することはもちろん、オフラインサポートもされているとのことです。
つまりFirestoreを利用することでローカルだけでなく、クラウドでDB管理ができるという事です。

今回は初めてのCloud Firestoreという事でFirestoreへデータの追加、更新、読み取り、削除の基本的な機能の実装方法をまとめていきたいと思います。

## データの追加
Firestoreの初期設定を終えると最初にこのような画面が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517666/6bfa02b8-760e-419a-1190-ca138563f6a6.jpeg" width=70%>
ここではデータの追加や削除などが直接行えます。
構造としてはコレクションとドキュメントの層があり、コレクションの中に複数のドキュメントを管理する構造になっています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517666/610c4120-b2ba-c2c9-0d49-4914e1ef5917.jpeg" width=50%>

今回はアプリ側の操作によってデータを追加していきたいと思います。
データの追加方法としては公式のドキュメントに以下のように記述されています。
>・コレクション内のドキュメントの識別子を明示的に指定し、そのデータを使用する。
>・コレクションに新しいドキュメントを追加する。この場合、ドキュメント識別子は Cloud Firestore により自動的に生成されます。
>・自動的に生成された識別子を持つ空のドキュメントを作成し、データを割り当てる。

単一のドキュメントを作成または上書きするには、set()関数を使用すれば実行できるとの事なのでset()を使用して追加していきます。

その前にFirestoreを使用する上でセキュリティルールは避けて通れないので一旦以下のリンクはざっと見ておく事をおすすめします。
・[スタートガイドライン](https://firebase.google.com/docs/rules/get-started?authuser=0)
・[セキュリティルール言語の詳細](https://firebase.google.com/docs/rules/rules-language?authuser=0)
・[基本的なセキュリティルール](https://firebase.google.com/docs/rules/basics?authuser=0)

[基本的なセキュリティルール](https://firebase.google.com/docs/rules/basics?authuser=0)にあるように、Firestoreのセキュリティルールの初期設定ではすべてのユーザーに対してアクセスを拒否しています。
セキュリティルールの詳細は上記を読んだ上でそれぞれ設定していただければと思います。
今回はとりあえず試してみるだけなので一旦すべてのユーザーに対してアクセスを許可させます。
(※セキュリティルールは下記のタブのルールにあります)
![スクリーンショット 2022-01-23 21.11.12.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517666/b2428715-1397-dc63-c6cd-568de3dfa5e2.jpeg)
デフォルトでは以下のように設定されており、アクセスできない状況になっていると思います。
(この状態でデータを追加しようとしてもコンソールに"failed: Missing or insufficient permissions"失敗：権限がないか不十分ですと表示されアクセスできません。)

```swift
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}
```
一旦falseをtrueに変更します。

```swift
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}
```
これでデータの追加ができるようになったので追加していきます。
以下のような適当な画面を作ります。
ここではtest buttonをタップすると現在アクセスしているアカウントのuidが表示されます。
またTextFieldに適当な文字を入力し、SaveをタップするとFirestoreに入力した文字が追加されるように実装していきます。

![スクリーンショット 2022-01-23 21.43.02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517666/5a8c8f99-3359-1edf-1beb-92a4992a98ed.jpeg)
※サインアウトのボタンが表示されていますがコード上に記載はないので実際は表示されません。

プロパティ

```swift
@State var uidText = "ここにIDが表示される"
@State var testInputText = ""
@State var fetchText = ""
```
画面と機能

```swift
VStack {
    //ID表示
    VStack {
        Button {
            guard let uid = Auth.auth().currentUser?.uid else {
                return uidText = "ログインしてないよ"
            }
            uidText = uid
        } label: {
            Text("test button")
        }
        Text(uidText)
    }
    //Save
    VStack {
        TextField(text: $testInputText) {
            Text("input Text")
        }
        .frame(width: UIScreen.main.bounds.width * 0.95)
        Button {
            Firestore.firestore().collection("users").document(uidText).setData(["testText": testInputText])
        } label: {
            Text("Save")
        }
    }
}
```

Firestore.firestore().collection("users").document(uidText).setData(["testText": testInputText])の部分がFirestoreにデータを追加するコードになります。
余談ですが、初めて触った時にたった1行で追加できる事に感動しました。

実際にtest buttonをタップして、TextFieldに"初めてのFirestore"と記入してSaveをタップします。
すると以下のように追加された事が確認できます。

![スクリーンショット 2022-01-23 22.08.55.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517666/9d35e847-6da8-9992-7171-8d9a461c8cc0.jpeg)


これでコレクションやドキュメントに追加された事が確認できます。

## データの取得
先ほど入力したデータを取得したいと思います。
###　特定のコレクションにある全てのドキュメントのデータを取得する

```swift
Firestore.firestore().collection("users").getDocuments { (success, error) in
    if let error = error {
        print(error.debugDescription)
    } else {
        let data = success!.documents.compactMap{ $0.data() }
        print(data)
    }
}
```

###　特定のコレクションにある一つのドキュメントのデータを取得する

```swift
Firestore.firestore().collection("users").document("001").getDocument { (success, error) in
    if let error = error {
        print(error.debugDescription)
    } else {
        let data = success!.data()
        print(data!)
    }
}
```

基本的なデータの取得は以上です。
## データの更新
```swift
Firestore.firestore().collection("users").document(uidText).updateData([
    "testText": "up data now"
]) { error in
    if let error = error {
        print(error.localizedDescription)
    } else {
        print("succes")
    }
}
```

ドキュメントにある一部のフィールドを更新できます。またフィールドがない場合ドキュメントに新たなフィールドとして追加されます。

## データの削除
ドキュメント削除

```swift
Firestore.firestore().collection("users").document(uidText).delete() { err in
    if let err = err {
        print("Error removing document: \(err)")
    } else {
        print("Document successfully removed!")
    }
}
```

フィールド削除

```swift
Firestore.firestore().collection("users").document(uidText).updateData([
    "testText": FieldValue.delete()
]) { err in
    if let err = err {
        print("Error updating document: \(err)")
    } else {
        print("Document successfully updated")
    }
}
```

フィールドのみ削除を行なった場合、上記の更新でフィールドに追加することも可能ですが、ドキュメントを削除した場合は更新したいドキュメントがない為、更新できません。
その場合は上記のデータの追加を行う方法で再度ドキュメントを作り直す必要があります。

## 最後に
個人的にはローカルDBのCore Dataより簡単に書ける印象でした。フロント側は非常に少ないコードでクラウド側でデータを管理してくれるのは非常に便利だなと感じました。
今回は試用する事が目的であったのでセキュリティルールにはよくない実装がありますが、今後はより実践的に使用していきたいのでもう少しセキュリティルールを学びたいと思います。

以上初めてのCloud Firestoreでした。