前書き
久々にペンギン村で記事を書きました(10ヶ月ぶり3記事目)、ナガクラ(@nagakuta)です。
今回はリハビリも兼ねて、RxSwift
とRealm
を組み合わせた小技を紹介します!
TL;DR
今すぐこれをコピペして Realm+Rx.swift
ファイルを作成するんだ!!
import RealmSwift import RxSwift extension Realm: ReactiveCompatible {} public extension Reactive where Base: Realm { /// Write objects in background queue func asyncWrite(objects: [Object]) -> Completable { let config: Realm.Configuration = self.base.configuration return Completable.create { (observer: @escaping PrimitiveSequenceType.CompletableObserver) -> Disposable in DispatchQueue(label: "hogehoge").async { autoreleasepool { do { let realm: Realm = try Realm(configuration: config) try realm.write { realm.add(objects, update: true) } observer(CompletableEvent.completed) } catch { observer(CompletableEvent.error(error)) } } } return Disposables.create() } } }
なぜ「書き込み完了イベント」を受けたいのか
だって、永続化されたデータが正しく更新されているのか分からないのは気持ち悪いじゃない!!?(個人の感想であり、所属する組織の公式見解ではありません)
Realm
で書き込み系の操作をしていていつも思うのが、「書き込み処理をかけたけど、実際に書き込みが完了しているのかは分からない…」ということです。
(そこは、「Storageの処理が完了したかどうかは操作側が知る必要ないのでは」というのもあるかとは思います。というか、そちらの考え方のほうが一般的…?🤔)
永続化しているアプリ設定を更新するとき、in-memoryでキャッシュを作成するときなど、正常に更新されたかどうか(言い換えると、更新エラーを検知できるかどうか)を知ることはつまり、正常系と準正常系/異常系のハンドリングを行えることに繋がります。
正常/異常ハンドリングによる結果でViewの表示を変更することで、ユーザフレンドリーなアプリに近づきます。
つまり、我々は「Realm
への書き込み処理が正しく完了されたことを知る必要がある」ということになります。
なぜRxSwift
なのか
だって、技術選定時にRxSwiftを使うかどうかって話になるくらい、現在のアプリ開発では準必須級なものじゃない!!?(個人の感想であり(ry)
完了したことを検知する方法にはいくつかあると思います。NotificationCenter
で通知を送信したり、完了時にDelegateメソッドを発火させたり。
ただし、それらは時に扱いづらく感じてしまう場面もあると思います。
その方法の一つとして存在するのがRx(Reactive Extensions)
です。
桃太郎の童話のように川(Stream)に桃(イベント、値)を流して、それを検知する(しかも非同期で)ことでデータの伝達の流れを作る、というものです。
僕自身、仕事でも個人開発でもRxSwift
を採用しています。面白いですよね、Streamを繋げるの。
Realm
を扱うクラス(レイヤー)から非同期で流されたイベントをViewで検知してからのハンドリング実装が行いやすい、というのが、僕がRealm
とRxSwift
を組み合わせる利点だなと思っています。
どうやって実装するの?
実装環境
Swift
: 4.2Realm
: v3.11.1RxSwift
: 4.4.0
Realmでのバックグラウンド更新
Realm
でバックグラウンド更新をかける方法については、こちらのCookbookを参考にしました。
import RealmSwift public extension Realm { /// Write objects in background queue func asyncWrite(objects: [Object]) { DispatchQueue(label: "hogehoge").async { autoreleasepool { do { let realm: Realm = try Realm(configuration: self.configuration) try realm.write { realm.add(objects, update: true) } } catch { // エラーハンドリング } } } } }
これで、Realm
のバックグラウンド更新が可能となりました。
注意点としては、この実装で更新可能なオブジェクトはスタンドアローンなものになります。
(スタンドアローンである = Realm
に管理されていない = Realm
から取得したオブジェクトではない、という意味です)
何故かと言うと、Realm
の管理下にあるオブジェクトに対して、取得処理が走っているスレッドとは別スレッドで更新処理をかけるには、ちょっとおまじないをかけてあげる必要があるからです。ちょっと面倒…😥
ちなみに、Realm
の管理下であるオブジェクトをスタンドアローンにするには、
// ↓この時点ではRealm管理下にある let object: HogeObject = /* Realmから取得 */ // ↓これでスタンドアローン化 let standaloneObject: HogeObject = HogeObject(value: object)
とする必要があります。
更新完了イベントの送信
さて、ではRxSwift
を利用して、書き込み完了通知の送信を実装します。
この実装で返すイベントはCompletable
です。先程、RxSwift
を説明する際に、
桃太郎のように川(Stream)に桃(イベント、値)を流して、
と説明しました。
通常、イベントには値(Int、String、Boolなど)が含まれる場合がほとんどですが、今回は「更新が完了したというイベント」を送信するので、特定の値を送信する必要がありません。その場合に利用するオペレータがCompletable
です。
RxSwift.Completable
を用いて書き込み完了通知を流す実装が以下になります(3分クッキング)👨🍳
import RealmSwift import RxSwift extension Realm: ReactiveCompatible {} public extension Reactive where Base: Realm { /// Write objects in background queue func asyncWrite(objects: [Object]) -> Completable { let config: Realm.Configuration = self.base.configuration return Completable.create { (observer: @escaping PrimitiveSequenceType.CompletableObserver) -> Disposable in DispatchQueue(label: "hogehoge").async { autoreleasepool { do { let realm: Realm = try Realm(configuration: config) try realm.write { realm.add(objects, update: true) } // ここで完了イベントを送信 observer(CompletableEvent.completed) } catch { // ここでエラーイベントを送信 observer(CompletableEvent.error(error)) } } } return Disposables.create() } } }
やっていることは、言ってしまえば単純で、do-catch内で問題なくバックグラウンド更新処理が走れば完了、エラーをcatchしたらエラーを送信するようにしているだけです。
これで、バックグラウンド更新の完了イベントを送信できるようになりました👍
Usage
このメソッドの使い方は、
let realm = try! Realm() realm.asyncWrite(objects: standaloneObjects) .subscribe(onCompleted: { // 完了イベントを受けて発火する何か }, onError: { (error: Error) in // エラーイベントを受けて発火する何か })
のようにして、完了/エラーイベントを受け取ったクラス/メソッドでアレコレするようにします。
まとめ
これでRealm
のバックグラウンド更新の完了を非同期で検知することができるようになりました👍
みんなもRealm
の更新処理をハンドリングして、いいアプリを作っていきましょう!!!(๑•̀ㅂ•́)و✧