RxSwiftでFirestoreのデータをリアルタイムでTableViewに反映する
RxSwift勉強中です
RxSwiftはずっと前から勉強しようと思っていたのですが、どこを見ても学習コストが高いと書かれていてビビってました。
ただインターン先でRxが使われていて勉強しないわけにもいかなくなったので、勉強会に参加してiOSエンジニアの方に教えてもらいながら簡単にRxを使ってFirestoreにデータを追加、取得をリアルタイムにTableViewに反映するアプリを作ってみました。
完成イメージ。
FirestoreにtextをPostし、その変更をリアルタイムで取得し、そのtextをTableViewCellに表示します。
まずはFirebaseの初期設定やCocoa PodsでFirestore、RxSwift、RxCocoaをpod installするなど諸々の設定を完了させます。
早速、まずはPostViewControllerにUITextFieldとUIButtonを用意します。
Rxを使ってボタンをタップした時の処理を書くとこうなります。簡単に書けて良いですね。
Model内のpostメソッドでFirestoreにデータを保存します。
class PostViewController: UIViewController { @IBOutlet weak var postLabel: UITextField! @IBOutlet weak var postButton: UIButton! let disposeBag = DisposeBag() let firestoreDataModel = FirestoreDataModel() override func viewDidLoad() { super.viewDidLoad() postButton.rx.tap.subscribe{ [weak self] _ in let text = self?.postLabel.text if text != "" { self?.firestoreDataModel.post(text: text!) self?.postLabel.text = "" } }.disposed(by: disposeBag) } }
そしてFirestoreにデータを追加、取得するためのFirebaseDataModelを用意します。
class FirestoreDataModel { lazy var db = Firestore.firestore() var strings = BehaviorRelay<[String]>(value: []) func post(text: String) { db.collection("sample").addDocument(data: ["text": text]) } func get() { db.collection("sample").addSnapshotListener{ (snapshot, error) in guard let value = snapshot else { print("nothing") return } value.documentChanges.forEach{ diff in if diff.type == .added { let data = diff.document.data() as? Dictionary<String, String> guard let sample = data else { return } guard let text = sample["text"] else { return } var oldStrings = self.strings.value oldStrings.append(text) self.strings.accept(oldStrings) } } } } }
postメソッドとgetメソッドのデータ追加、取得の部分は公式ドキュメント通りです。
addDocumentメソッドでデータを追加して、addSnapshotListenerで変更を監視しています。
変数stringsは配列の文字列型BehaviorRelayです。
このBehaviorRelayですが、onErrorもonCompletedも流さないObservableです。つまり予期せず終了することなくイベントを流し続けられます。
Rxの基本的な仕組みはBehaviorRelayのようなObservableから流れてくるイベントを、別の場所でsubscribeすることでそのイベントを受け取るというものです。
RelayにはPublishRelayとBehaviorRelayがあって、その違いはBehaviorRelayは初期値や現在値を持っており、PublishRelayは持ってません。
今回で言えば、『Firestoreからデータを取ってきてそれをoldStringsにappendしたという変更』をこのModelからViewModelへ現在値oldStringsと一緒にイベントとして流しています。
そして次はそのイベントを受け取るFirebaseDataViewModelです。
class FirestoreDataViewModel { let firestoreDataModel = FirestoreDataModel() var texts: BehaviorRelay<[String]> var disposeBag = DisposeBag() init() { firestoreDataModel.get() texts = BehaviorRelay(value: []) firestoreDataModel.strings.bind(to: texts).disposed(by: disposeBag) } }
先ほどのfirestoreDataModel内のBehaviorRelayであるstringsを、自クラス内のBehaviorRelayであるtextsにbindしています。
これでstringsの変更がtextsに反映されるようになりました。
最後にそのデータを表示するためのTableViewController。
class TableViewController: UITableViewController { let firestoreDataViewModel = FirestoreDataViewModel() var texts = [String]() let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() firestoreDataViewModel.texts .subscribe(onNext: { [weak self] texts in self?.texts = texts self?.tableView.reloadData() }).disposed(by: disposeBag) } // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return texts.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = texts[indexPath.row] return cell } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 120 } }
ViewModel内のBehaviorRelayであるtextsからイベントが流れてきたら、それを受け取って、自クラス内のtexts配列に代入し、TableViewを更新しています。
これでこんな感じにTableViewにPostした文字列がリアルタイムに反映されるようになりました。
まだ完璧に理解したとは言い難い状況ではありますが、積極的に自分で使いながら慣れていきたいと思います。