そこらへんの大学生のブログ

iOS開発やったり、海外に住んだり、大学生やったりしてます。

RxSwiftでFirestoreのデータをリアルタイムでTableViewに反映する

RxSwift勉強中です

RxSwiftはずっと前から勉強しようと思っていたのですが、どこを見ても学習コストが高いと書かれていてビビってました。

ただインターン先でRxが使われていて勉強しないわけにもいかなくなったので、勉強会に参加してiOSエンジニアの方に教えてもらいながら簡単にRxを使ってFirestoreにデータを追加、取得をリアルタイムにTableViewに反映するアプリを作ってみました。

完成イメージ。
FirestoreにtextをPostし、その変更をリアルタイムで取得し、そのtextをTableViewCellに表示します。

f:id:Rwkabms:20190421015253p:plain:w250:h450 f:id:Rwkabms:20190421015306p:plain:w250:h450

まずは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した文字列がリアルタイムに反映されるようになりました。

f:id:Rwkabms:20190421213858p:plain:w200:h450 f:id:Rwkabms:20190421214017p:plain:w460:h450

 
まだ完璧に理解したとは言い難い状況ではありますが、積極的に自分で使いながら慣れていきたいと思います。