【Firebase✖️SwiftUI】構造体のマッピングで楽にデータの取得や更新を行う
SwiftUIでFirebaseを使っているときに、フィールドの値を1つ1つ変数に入れて取得してたりしませんか?また、登録の時は、Dictionaryからいちいち登録してませんか?
let ref = db.collection("users").document("id")
ref.getDocument() {
document, error in
let data = document?.data()
let name = data?["name"] ?? ""
let about = data?["about"] ?? ""
}
FirebaseFirestoreSwift & async/awaitを使うと、
いちいち取得して構造体に詰めたりしなくても一発でできて便利!(イエーイ🙌)
let ref = db.collection("users").document("id")
let user = try await ref.getDocument()
.data(as: User.self)
FirebaseFirestoreSwift
ということでFirebaseFirestoreSwift(長いので以下「FFS」にします。)を使って構造体のマッピング(以下「変換」)をしていきたいと思います。
特徴
Codable[1]に準拠した構造体でFirestoreのメソッドを呼び出すとFirestoreとやり取りするデータを構造体に変換され扱えるため操作が簡単に行えます。
最初に示したコード例のこの部分がそれにあたります。
.data(as: User.self)
便利なポイントとしては、構造体のプロパティを変更すると対応できていない箇所ではコンパイルエラーになるため、修正する必要のある箇所を見つけやすいのがいいなと思います。
さらにFFSとは関係ないですが、データの取得などの処理がasync/awaitで書けるようになったことでCompletionでの書き方よりも見やすく分かりやすいのも好きなポイントです。
それでは具体的に使えるようにしていきます。
1. FirebaseFirestoreSwiftを追加する
使うにはパッケージFirebaseFirestoreSwift
をプロジェクトに追加します。
SwiftPMならパッケージに追加するだけです。
2. Codableに準拠させる
Codableに準拠した構造体を作成します。CodableはEncodable, Decodableを両方扱えるプロトコルです。
import FirebaseFirestoreSwift // 必要
struct User: Identifiable, Codable {
@DocumentID var id: String?
var name: String
@ServerTimestamp var createdAt: Timestamp?
@ServerTimestamp var updatedAt: Timestamp?
}
@DocumentID
のような特殊なプロパティーラッパーを使うのにimport FirebaseFirestoreSwift
が必要です。プロパティラッパーを使うことでデータを変換する際の挙動が少し変わります。
何が使えるか、より詳しく知りたい場合はSwift CodableでCloud Firestoreのデータをマッピングするを参照してください。ServerTimestampを使うことで日付の更新がやりやすかったり、ネストした際構造体ではidが使えないなど色々書いてあります。
3. メソッド呼び出しの際に構造体やインスタンスを渡す
各処理で実際に呼び出して試していきます。User
の中身は先ほどのUser.swift
です。
今回はCompletionでのコールバックを使った書き方ではなく、async/awaitに対応した書き方で記載していきます。(エラーハンドリングは省略)
getDocument
func getDocument() async throws -> User {
let ref = db.collection("users").document("id")
return try await ref.getDocument()
.data(as: User.self)
}
.data(as: Decodable.Protocol)
が変換してくれている部分です。UserはDecodableに準拠しているためここでUser構造体のインスタンスが生成され返ってきます。変換できない場合は例外が投げられます。
getDocuments
func getDocuments() async throws -> [User] {
let ref = db.collection("users")
return try await ref.getDocuments()
.documents
.compactMap { try? $0.data(as: User.self)}
}
documentsでドキュメントの一覧を取得できます。
さらに、compactMapを使いひとつづつUserインスタンスに変換していってます。
compactMapで実行する変換処理をtry
ではなくtry?
にしてると変換できない際、
配列の要素から削ってくれるのため、自分はよくcompactMap使ってます。
addDocument
func addDocument(user: User) throws {
let ref = db.collection("topics")
try ref.addDocument(from: user)
}
async/awaitは必要ありません。
fromでUserのインスタンを渡してあげることで、あとは勝手に変換してリクエストしてくれます。
setData
func setData(user: User) throws {
let ref = db.collection("users").document("id")
try ref.setData(from: user)
}
addDocumentと同様。
updateData
func setData(user: User) throws {
let ref = db.collection("users").document("id")
try ref.setData(from: user, merge: true)
}
fromで変換できるタイプのupdateDataメソッドは探してみた感じなさそうでした。(あったら教えてください)
setDataにはmergeオプションがあり特定のフィールドのみの更新もできるため更新は主にsetDataを使ったやり方で良さそうかなと思いました。
delete
func delete() async throws {
let ref = db.collection("users").document("id")
try await ref.delete()
}
deleteは変換するデータがないので、あまりFFSと関係ないですがasync/awaitで書くとこんな感じです。
Firestoreのデータが構造体に変換される時とされない時
データによって構造体に変換される時とされない時があり、よく分からなかったのでまとめてみました。
変換できる | 変換できない |
---|---|
ドキュメントのフィールドを構造体側がプロパティとして定義していない時 | 構造体のプロパティをドキュメント側でフィールドを作成していない時 |
あくまでSwift側で変換できるかどうかなので、構造体のプロパティをFirestoreのドキュメントが全てフィールドとして持っていれば変換できます。また、Firestoreだけにしかないフィールドがあったとしても、基本的に構造体で定義されているプロパティがドキュメントのフィールドとして存在していれば変換できます。
逆に構造体のプロパティをドキュメント側で持っていない場合はエラー変換ができないですが、そのプロパティがOptionalなら変換できます。Optionalではなくデフォルト値をプロパティに設定したりもしてみましたが、やはりドキュメントに値がないため変換できませんでした。
参考
最後に
FirebaseFirestoreSwiftとasync/awaitを使うことで以前の書き方よりもすっきり書けるのでよかったです。
【宣伝】
エンジニアと繋がれるプラットフォームMMMMを作りました!
どうやってエンジニアと繋がったらいいか分からない、どうすれば強くなれるのか分からない。そう言った悩みを解決できる場所です。
よかったら下記リンクから参加してください。
終わり。
Discussion