NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

FirestoreがSwiftのasync/awaitに対応したので試してみた

前回の記事ではFirebase Apple SDK 9.0.0でasync/awaitに対応したのでAuthenticationで試してみた記事を書きましたが 今回はFirestoreで試してみました。

前回の記事

tech.nri-net.com

概要: 今回試した事

5/11から開催かれたGoogle I/O 2022でFirebaseについての発表、説明がありました。
詳細を知りたい方は以下動画と記事を参照ください。

io.google

firebase.blog

今回はFirebaseがSwift対応したことによってasync/awaitで処理する事が可能になりましたので以下Firestoreの機能を試してみました。

  • 追加
  • 更新
  • 取得
  • 削除

前提知識: Firestoreとは

Googleが提供している"クラウドでデータ管理できる"サービスです。 公式では以下のように紹介されています。

Cloud Firestore は、Firebase と Google Cloud からのモバイル、ウェブ、サーバー開発に対応した、柔軟でスケーラブルなデータベースです。
Firebase Realtime Database と同様に、リアルタイムリスナーを介してクライアント アプリ間でデータを同期し、モバイルとウェブのオフラインサポートを提供します。
これにより、ネットワークレイテンシやインターネット接続に関係なく機能するレスポンシブアプリを構築できます。
Cloud Firestore は、その他のFirebaseおよびGoogle Cloud プロダクト(Cloud Functions など)とのシームレスな統合も実現します。
Cloud Firestore  |  Firebase Documentation

想定読者として

  • Swiftに関して触ったことがある。もしくは多少の知識がある。
  • Firebase iOS SDKを触ったことがある。

方を想定していますのでFirebaseの導入方法やasync/awaitの解説などはここでは割愛させていただきますのでご了承ください。

Firebaseの導入やasync/awaitについて詳しく知りたい方は前回の記事で個人的におすすめな記事やドキュメントを紹介していますのでそちらを参考にしていただければと思います。

FirebaseがSwiftのasync/awaitに対応したのでFirebase Authenticationで試してみた - NRIネットコムBlog

実践: Firestore

ここからは実際に試してみたいと思いますが、その前にまずFirestoreで管理したいDataを適当にstructで作成しておきます。
この時Codableに準拠させておくとカスタムオブジェクトとして非常にすっきりと書く事ができます。
またSwiftUIを利用してForEachで取得したDataを繰り返し表示させたい場合はIdentifiableに準拠させる必要があります。
どちらも必須では無いので必要に応じて準拠していただければと思いますが今回のサンプルではどちらも準拠した形のものとなっています。
また余談ですがCodableはEncodableおよびDecodableのタイプエイリアスです。(簡単に言うとEncodable, Decodableをまとめたもので機能的にはEncodable, Decodableを準拠させているのと同じです)
Identifiableはデータを識別可能にするためのプロトコルです。プロトコルの要件として識別子である"id"と言うプロパティを必ず持たせる必要があります。id設定に関して指定はありませんがここではUUIDを利用して設定します。

以下共通して使用するCodeです。

//インスタンスを初期化
let db = Firestore.firestore()

Userというstructを作成して使用していきます。
上記にも説明した通り、必須ではなく、MapやDictionaryを使用してFirestoreのdocumentを定義する場合は必要ではありません。

public struct User: Codable, Identifiable {
    public var id = UUID().uuidString
    let name: String
    let address: String
    let age: Int
}

追加

エラー処理も含めて追加する場合はこんな感じです。

func setUser() async {
    let user = User(name: "oka", address: "tokyo", age: 34)
    do {
        try await db.collection("users").document(user.id).setData([
        "name": user.name,
        "address": user.address,
        "age": user.age
        ])
    } catch {
        //何かしらのエラーの処理を書く
    }
}

エラー処理をここではしない場合はthrowsを付与してエラーを投げるようにします。
その場合setUser()を使用するメソッドでdo-try-catchの形で処理して下さい。

func setUser() async throws {
    let user = User(name: "oka", address: "tokyo", age: 34)
    try await db.collection("users").document(user.id).setData([
        "name": user.name,
        "address": user.address,
        "age": user.age
    ])
}

また今回はCodableに準拠したstructを用意していますので使用するとこんな感じで書けます。

func setUser() async throws {
    let user = User(name: "oka", address: "tokyo", age: 34)
    try db.collection("users").document(user.id).setData(from: user)
}

実際にメソッドの中身は2行だけです。
ここまでシンプルに書けるのはすごいです!

更新

更新もシンプルです。
※idは登録したUUIDに置き換えて書き換える必要があります。

func upDate() async throws {
    try await db.collection("users")
        .document(id)
        .updateData(["name" : "yuji"])
}

取得

取得する方法は1つのドキュメントや複数のドキュメントと色々ありますが今回は上記で作成したstructを使用して取得してみます。

func getUser() async throws {
    let document = try await db.collection("users").document(id).getDocument(as: User.self)
    print(document.name)
    print(document.address)
    print(document.age)
}
取得のtips

getDocumentにはsource オプションを設定する事ができます。
これはFirestore側でいい感じにキャッシュの制御をしてくれる機能で書き方はこんな感じです。

func getUser() async throws {
    let document = try await db.collection("users").document(id).getDocument(source: .default)
    let user = try document.data(as: User.self)
    print(user.name)
    print(user.address)
    print(user.age)
}

またそれぞれのモードは以下のようになっています。

オプション 説明
.default Causes Firestore to try to retrieve an up-to-date (server-retrieved) snapshot, but fall back to returning cached data if the server can't be reached.
翻訳
Firestoreが最新の(サーバーで取得した)スナップショットを取得しようとしますが、サーバーに到達できない場合は、キャッシュされたデータを返すようにフォールバックします。
.server Causes Firestore to avoid the cache, generating an error if the server cannot be reached. Note that the cache will still be updated if the server request succeeds. Also note that latency-compensation still takes effect, so any pending write operations will be visible in the returned data (merged into the server-provided data).
翻訳
Firestoreにキャッシュを回避させ、サーバーに到達できない場合はエラーを生成します。サーバーリクエストが成功した場合でも、キャッシュは更新されることに注意してください。また、レイテンシー補正は引き続き有効であるため、保留中の書き込み操作はすべて、返されたデータ(サーバー提供のデータにマージされます)に表示されることに注意してください。
.cache Causes Firestore to immediately return a value from the cache, ignoring the server completely (implying that the returned value may be stale with respect to the value on the server). If there is no data in the cache to satisfy the getDocument[s] call, DocumentReference.getDocument() will return an error and QuerySnapshot.getDocuments() will return an empty QuerySnapshot with no documents.
翻訳
サーバーを完全に無視して、Firestoreがキャッシュから値をすぐに返すようにします(サーバー上の値に対して戻り値が古くなっている可能性があることを意味します)。 getDocument [s]呼び出しを満たすデータがキャッシュにない場合、 DocumentReference.getDocument()はエラーを返し、 QuerySnapshot.getDocuments()はドキュメントのない空のQuerySnapshotを返します。

削除

ドキュメントの削除に関しては一行で済みます。

func deleteUser() async throws {
    try await db.collection("users").document(id).delete()
}

実際に試してみて

Firebase同様、非常にコードがシンプルになりました!
またSwift対応したことでコールバックではなくasync/awaitで書けるのもいいですね!
引き続き個人開発でも使用していきたいと思いますのでまた何か新しい発見があれば引き続き記事を書きたいと思います!
今回はとりあえず試してみました!!という記事でした!

おまけ

今回エラーハンドリングはしませんでしたがAuthentication同様Firestoreにもエラーコードがあるのでコチラ一覧にしておきます。
よければ参考に使って下さい!

No. ErrorCode 説明
0 OK 操作は正常に完了しました。NSErrorオブジェクトにこの値のコードが含まれることはありません。
1 cancelled 操作はキャンセルされました(通常は呼び出し元によって)。
2 unknown 不明なエラーまたは別のエラードメインからのエラー。
3 invalidArgument クライアントが無効な引数を指定しました。これはFailedPreconditionとは異なることに注意してください。InvalidArgumentは、システムの状態に関係なく問題のある引数(たとえば、無効なフィールド名)を示します。
4 deadlineExceeded 操作が完了する前に期限が切れました。システムの状態を変更する操作の場合、操作が正常に完了した場合でも、このエラーが返されることがあります。たとえば、サーバーからの正常な応答は、期限が切れるまで十分に遅れている可能性があります。
5 notFound 要求されたドキュメントが見つかりませんでした。
6 alreadyExists 作成しようとしたドキュメントはすでに存在します。
7 permissionDenied 呼び出し元には、指定された操作を実行する権限がありません。
8 resourceExhausted 一部のリソースが使い果たされているか、ユーザーごとの割り当てが不足しているか、ファイルシステム全体の容量が不足している可能性があります。
9 failedPrecondition システムが操作の実行に必要な状態にないため、操作は拒否されました。
10 aborted 通常、トランザクションの中止などの同時実行の問題が原因で、操作が中止されました。
11 outOfRange 有効範囲を超えて操作を試みました。
12 unimplemented 操作が実装されていないか、サポート/有効化されていません。
13 internal 内部エラー。基礎となるシステムによって期待されるいくつかの不変条件が壊れていることを意味します。これらのエラーのいずれかが表示された場合は、何かが非常に壊れています。
14 unavailable このサービスは現在ご利用いただけません。これは一時的な状態である可能性が高く、バックオフで再試行することで修正できます。
15 dataLoss 回復不能なデータの損失または破損。
16 unauthenticated リクエストには、操作の有効な認証クレデンシャルがありません。