Firestore SDKをiOSで使おう
Firestore
簡単に言うとオンラインで利用できるデータベース。
ローカルに保存するだけなら Realm という選択肢があるが、オンラインでごにょごにょしようとしたら Firestore が一番なのではないかと思う。Realm もオンラインデータベースがあるけどドキュメントを読んでないのでよくわからない。
せっかくなので新しいことに手を出そうと Firestore を選択した。
Firestore SDK
Swift Package Manager でインストールする。いろいろプロダクトはあるのだがFirebaseFirestoreSwift
だけ選んでおけば良い。FirestoreAnalytics
も便利なので自分はこれもインストールした。必要かどうかはわからない。
最後にウェブ上で Firebase の登録を済ませてGoogleService-Info.plist
をプロジェクトに突っ込んで準備は完了。
FSCodable
Firestore を圧倒的に使いやすくするためのFSCodable
というCodable
+Identifiable
な独自プロトコルを作成する。
import Foundation
protocol FSCodable: Codable, Identifiable {
var id: String? { get }
}
extension FSCodable {
var id: String? { nil }
}
そしてExtension
でid
のデフォルト値がnil
になるようにする。ここのid
はIdentifiable
に由来するユニークな値なので、被らないような値が望ましい。
Struct
import Foundation
struct User: FSCodable {
let name: String
let age: Int
}
extension User {
var id: String? { name }
}
次に Firestore に保存したい構造体を考える。構造体 →JSON→Firestore 保存はこのあと解説する Manager クラスが全て行うので、ここではプライマリキーと保存したいプロパティだけを考える。
Manager
最後に Manager クラスを定義しておしまいである。
Generics を利用しているのでFSCodable
に準拠している構造体ならなんでも書き込めるし、何でも読み込める。
読み込みについて
読み込みは非同期なのでreturn
できないことに注意。受け取るにはCombine
を使ってsink
する必要がある。
構造体にプライマリキーが指定されていればそれで書き込み、指定されていなければ Firebase が自動で設定するユニークな ID が割り当てられる。
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
import Combine
class FirestoreManager {
private let firestore: Firestore = Firestore.firestore()
private let encoder: Firestore.Encoder = Firestore.Encoder()
private let decoder: Firestore.Decoder = Firestore.Decoder()
init() {}
/// データ書き込み
func create<T: FSCodable>(_ object: T, merge: Bool = false) throws {
let data = try encoder.encode(object)
if let primaryKey = object.id {
firestore.collection(String(describing: T.self)).document(primaryKey).setData(data, merge: merge)
} else {
firestore.collection(String(describing: T.self)).document().setData(data, merge: merge)
}
}
/// プライマリキーを指定してデータ取得
func object<T: FSCodable>(type: T.Type, primaryKey: String) -> AnyPublisher<T, FIError> {
Future { [self] promise in
firestore.collection(String(describing: T.self)).document(primaryKey).getDocument(completion: { [self] (document, _) in
guard let document = document, let data = document.data() else {
// 適当にEnumで設定したエラー
promise(.failure(.notfound))
return
}
do {
promise(.success(try decoder.decode(T.self, from: data)))
} catch {
// 適当にEnumで設定したエラー
promise(.failure(.undecodable))
}
})
}
.eraseToAnyPublisher()
}
/// 指定された構造体のデータを全て取得
func objects<T: FSCodable>(type: T.Type) -> AnyPublisher<[T], FIError> {
Future { [self] promise in
firestore.collection(String(describing: T.self)).getDocuments(completion: { [self] (snapshot, _) in
if let snapshot = snapshot, !snapshot.documents.isEmpty {
promise(.success(snapshot.documents.compactMap({ try? decoder.decode(T.self, from: $0.data()) })))
} else {
// 適当にEnumで設定したエラー
promise(.failure(.notfound))
}
})
}
.eraseToAnyPublisher()
}
}
エラーを返す場合があるので、エラーは以下のように定義しました。
import Foundation
enum FIError: Error {
case notfound
case undecodable
}
今後の展望
今回は全部取得するか、一見取得するかにしか対応したコードになっていないが、改良すればwhere
に対応したりできると思います。