FireStoreからRxSwiftで値を参照
こんなふうにしたいために
private static let database = Firestore.firestore()
static func getDetails(prefecture_id: Int) -> Observable<[Kouyou]> {
return database.collection("/kouyou")
.whereField("prefecture_id", isEqualTo: prefecture_id)
.rx.getDocuments() // QuerySnapshot
.map { $0.documents } // [QueryDocumentSnapshot]
.map { $0.compactMap{ doc in return try? Kouyou.init(from: doc) } }
}
RxFirebase/Firestore というのがあったはずだけど最新の環境では動かなかったので同じように使えるものを考えてみた。
設計
Firestore の「1回だけ getDocuments する非同期処理」を、RxSwift の Observable として扱えるようにラップ
1 拡張対象
extension Reactive where Base: Query {
- Reactive は RxSwift が提供するラッパー
- where Base: Query なので Firestore の Query を拡張
- これにより query.rx.getDocuments() のように使える
2 関数定義
func getDocuments() -> Observable<QuerySnapshot>
- Firestore の getDocuments を呼び出して、その結果を RxSwift の Observable に変換する関数
- 戻り値は Observable(Firestore の結果を流すストリーム)
3 Observable の生成
return Observable.create { observer in
self.base.getDocuments { snapshot, error in
- Observable.create で カスタムな Observable を作成
- self.base は拡張元の Query インスタンス(Firestore のクエリ)
- Firestore の非同期 API(クロージャで結果が返る)をRx の仕組みに変換
4 Firestore の結果を Observer に流す
if let error = error {
observer.onError(error) // エラーがあればストリームに流す
} else if let snapshot = snapshot {
observer.onNext(snapshot) // データを流す
observer.onCompleted() // 1回きりで終了
}
- 成功時 → onNext(snapshot) で結果を流し、onCompleted() で終了
- 無限に流れ続けるストリームではなく「一度だけ」
- 失敗時 → onError(error) でストリームをエラー終了
5 リソース解放
return Disposables.create()
- Observable の購読が dispose されたときのクリーンアップ処理
- 今回は特別な解放処理が不要なので空の Disposables.create() を返却
作成したコード
extension Reactive where Base: Query {
/// Firestore の getDocuments を 1回だけ Observable で流す
func getDocuments() -> Observable<QuerySnapshot> {
return Observable.create { observer in
self.base.getDocuments { snapshot, error in // 元の Query インスタンスを self.base として参照
if let error = error {
observer.onError(error)
} else if let snapshot = snapshot {
observer.onNext(snapshot)
observer.onCompleted() // ここで終了
}
}
return Disposables.create()
}
}
}
利用例
Firestore.firestore()
.collection("/kouyou")
.whereField("prefecture_id", isEqualTo: prefecture_id) // Query を作成
.rx.getDocuments() // 拡張した Observable 化メソッド
.map { $0.documents } // QuerySnapshot を [DocumentSnapshot] に変換
.subscribe(onNext: { docs in // 取得した配列を受け取る
print(docs) // 成功してドキュメントが来たとき
},
onError: { error in
print("エラー発生: \(error)") // Firestore 側で失敗したとき
},
onCompleted: {
print("読み取り完了") // 正常に完了したとき
})
.disposed(by: disposeBag) // 購読のライフサイクル管理
またはこんなのを作って
import RxSwift
import RxCocoa
enum Result<Response> {
case succeeded(Response)
case failed(Error)
}
extension ObservableConvertibleType {
func resultDriver() -> Driver<Result<Element>> {
return self.asObservable()
.map { Result.succeeded($0) }
.asDriver { Driver.just(Result.failed($0)) }
}
func materializeAsDriver() -> Driver<Event<Element>> {
return self.asObservable()
.materialize()
.asDriver(onErrorDriveWith: .empty())
}
}
extension SharedSequence {
/// split result to Element and Error
///
/// - Parameter result: Driver<Result<Element>>
/// - Returns: Driver<Element>, Driver<Error>
static func split(result: Driver<Result<Element>>) -> (response: Driver<Element>, error: Driver<Error>) {
let responseDriver = result.flatMap { result -> Driver<Element> in
switch result {
case .succeeded(let response):
return Driver.just(response)
case .failed:
return Driver.empty()
} }
let errorDriver = result.flatMap { result -> Driver<Error> in
switch result {
case .succeeded:
return Driver.empty()
case .failed(let error):
return Driver.just(error)
} }
return (responseDriver, errorDriver)
}
}
こうする
import RxSwift
import RxCocoa
import Firebase
import FirebaseFirestore
class KouyouService {
private static let database = Firestore.firestore()
static func getDetails(prefecture_id: Int) -> Observable<[Kouyou]> {
return database.collection("/kouyou")
.whereField("prefecture_id", isEqualTo: prefecture_id)
.rx.getDocuments() // QuerySnapshot
.map { $0.documents } // [QueryDocumentSnapshot]
.map { $0.compactMap{ doc in return try? Kouyou.init(from: doc) } }
}
}
// FireStoreから最新データを取得
let (list, error) = Driver.split(result: KouyouService.getDetails(prefecture_id: self.prefecure_id).resultDriver())
list.asDriver().drive(onNext: {[unowned self] details in
let data = details.compactMap { self.convertDetail($0) }
}).disposed(by: rx.disposeBag)
error.asDriver().drive(onNext: { error in
print(error)
}).disposed(by: rx.disposeBag)