【Swift】iOSアプリでFirestoreからデータを取得する方法

昔、AWSを使って個人アプリを開発していたのですが、APIを作ってEC2に配置して、RDS設定して・・・
とにかく色々大変だったので、もっと簡単にCRUD機能を実現したいなと思いました

今更かもしれませんが、**FirestoreでAPIを作らずに簡単にCRUD機能を実現できる**ことを知ったので、今回はデータを取得するところまでをやってみたいと思います

ソースコードはこちらから↓

https://github.com/riku3/firestore-sample-app

##環境
Xcode12.4
Swift5

#Firebaseと連携する
下記の公式ドキュメントに沿って、iOSアプリとFirebsseの連携をしてみる

https://firebase.google.com/docs/firestore/quickstart?hl=ja#ios

##新規プロジェクト作成
Firebaseコンソール画面から新規にプロジェクトを追加する

https://console.firebase.google.com/u/0/?hl=ja

プロジェクト追加
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/c0a251a6-1475-e885-47e8-e11f2412778b.png">

任意のプロジェクト名を記載して続行
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/fc83e79b-c133-5c84-e06a-1eca0a76da15.png">

今回はGoogleアナリティクスへの連携はOFFにして、プロジェクトを作成
ONにすると、次の画面で連携するGoogleアカウントを指定する必要があります
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/c2fc44ab-75d1-a240-4145-b9842cfd0b0d.png">

新規にプロジェクトが作成されました
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/f4d01af6-5554-c566-8513-a34169f30f90.png">
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/a243d777-a9ea-e190-8ea2-9c9850fb175e.png">

##Firestoreでデータベース作成
早速、FireStoreでデータを作成していきます

コンソール画面からFirestore Databaseを選択してデータベースを作成
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/c853226e-6f70-e57e-c881-122fa26b0f6d.png">

テストモードを選択して次に行きます
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/a81a6190-316d-b5b6-96b4-794713e857c0.png">
公式ドキュメントによると、**本番環境モード（ドキュメントではロックモード）では、モバイルアプリから直接Firestoreのデータを参照できない**ため、APIやBatchを経由する必要があるみたいです。
そのため、今回はモバイルアプリから直接データを参照できるテストモードを選択します
>ロックモード
モバイルおよびウェブ クライアントからのすべての読み書きを拒否します。認証されたアプリケーション サーバー（C#、Go、Java、Node.js、PHP、Python、Ruby）は引き続きデータベースにアクセスできます。
https://firebase.google.com/docs/firestore/quickstart?hl=ja#create

ロケーションをは利用場所から一番近い**asia-northeast1(東京)**を選択します
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/7edeafcc-c181-8819-56ac-61fc131dc905.png">
日本にはロケーションが東京と大阪にあるみたいです
**ロケーションは一度しか設定できないため要注意です**
>asia-northeast1 東京
asia-northeast2  大阪
https://firebase.google.com/docs/firestore/locations?hl=ja#types

##データ追加
今回はコンソール画面からPersonデータを2件追加しました
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/d15ece08-c0a3-a126-9448-860c58fdb7d6.png">
Firestoreだと配列の中に配列を入れることができないため、今回はhobbies配列の中にmapタイプでnameやyearのデータを入れました。referenceタイプを指定すると、別のドキュメントを指定することも可能です。

ちなみに、コレクションやドキュメントの説明については、以下の記事がとてもわかりやすかったです

https://qiita.com/1amageek/items/d606dcee9fbcf21eeec6

##ライブラリをインストール
Firestoreのライブラリを入れていきます。今回は`CocoaPods`を利用します。
Podsの初期化については以下の記事が参考になると思います。

https://qiita.com/ShinokiRyosei/items/3090290cb72434852460

指定のライブラリをPodfileに追加します
※Swiftを利用する場合は、`FirebaseFirestoreSwift`も追加するらしい。

```
pod 'Firebase/Firestore'
# Optionally, include the Swift extensions if you're using Swift.
pod 'FirebaseFirestoreSwift'
```
インストールします

```
pod install
```
##Firebseの初期化
Firestoreを利用する際に初期化が必要となるため、`Appdelegate`で初期化しておきます

```swift
import UIKit
import Firebase

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Firebase初期化
        FirebaseApp.configure()
        return true
    }
}
```
ビルドしたらコンパイルエラーになりました・・・

```terminal
Terminating app due to uncaught exception 'com.firebase.core', reason: '`[FIRApp configure];` (`FirebaseApp.configure()` in Swift) could not find a valid GoogleService-Info.plist in your project. Please download one from https://console.firebase.google.com/.'
```
そもそもFirebaseにiOSプロジェクトを追加していなかったです
初期設定の際に`GoogleService-Info.plist`を追加するのですが、今回はそのファイルがなくてエラーとなっています

##FirebaseにiOSプロジェクト追加
少し話が前後してしまい申し訳ないですが、FirebaseにiOSプロジェクトを追加します

https://firebase.google.com/docs/ios/setup?hl=ja

コンソール画面でiOSを選択
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/dd107f04-7ede-1306-9ba4-f7e0e88680f9.png">

BundleIDとニックネームを記載
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/12a78ddd-31c4-f55f-e97e-0ac7e5185d8f.png">

GoogleService-Info.plistをダウンロード
後続は前述したようにpodでインストールとFirebaseの初期化処理なので、割愛します
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/6c693a85-d065-f73d-d6ae-a83da47503cd.png">

GoogleService-Info.plistをプロジェクトのRoot配下に追加
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480691/56cce699-c00a-e78a-ae7b-0dc1f08e4c27.png">
無事にビルドできました(失礼しました！)

##Firestoreからデータ取得
Firestoreで追加したデータを構造体(Person,Hobby)に格納しました
データは非同期で取得されます

```swift:ViewController.swift
import UIKit
import Firebase

struct Person {
    var name: String
    var age: Int
    var hobbys: [Hobby]
}

struct Hobby {
    var name: String
    var year: Int
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // リスト初期化
        var persons: [Person] = []
        
        // FirestoreのDB取得
        let db = Firestore.firestore()
        // personsコレクションを取得
        db.collection("persons").getDocuments() { collection, err in
            // エラー発生時
            if let err = err {
                print("Error getting documents: \(err)")
            } else {
                // コレクション内のドキュメントを取得
                for document in collection!.documents {
                    // hobbiesフィールドを取得
                    guard let hobbyDicList: [[String : Any]] = document.get("hobbies") as? [[String : Any]] else {
                        continue
                    }
                    
                    // リスト初期化
                    var hobbies: [Hobby] = []
                    
                    // hobbies内のフィールドを取得
                    for hobbyDic in hobbyDicList {
                        guard let hobbyName = hobbyDic["name"] as? String ,
                              let hobbyYear = hobbyDic["year"] as? Int else {
                            continue
                        }
                        // Hobbyを作成
                        let hobby = Hobby(name: hobbyName, year: hobbyYear)
                        // リストに追加
                        hobbies.append(hobby)
                    }
                    
                    // Personフィールドを取得
                    guard let personName = document.get("name") as? String,
                          let personAge = document.get("age") as? Int else {
                        continue
                    }
                    
                    // Personを作成
                    let person = Person(name: personName, age: personAge, hobbys: hobbies)
                    
                    // リストに追加
                    persons.append(person)
                }
            }
            // コンソール出力
            print(persons)
        }
    }
}
```
出力結果より、Firestoreに追加したデータを無事に取得することができました！

```
[firestore_sample_app.Person(name: "jiro", age: 35, hobbys: [firestore_sample_app.Hobby(name: "climbing", year: 10)]), firestore_sample_app.Person(name: "taro", age: 20, hobbys: [firestore_sample_app.Hobby(name: "baseball", year: 5), firestore_sample_app.Hobby(name: "tennis", year: 8)])]
```
##おわりに
今回、Firestoreを利用してみましたが、とても使いやすかったです
シンプルな機能ならFirebaseだけで事足りそうだなと感じました

ユーザー認証、ストレージ機能など他にも様々な機能が用意されているので、色々試してみたいと思います
(サーバ側はFirebaseに任せてiOSアプリに注力できそうです)


最後まで読んで頂きありがとうございます！