ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

【iOS】RxSwift + Realmでバックグラウンドでの書き込み完了イベントを受け取る

前書き

久々にペンギン村で記事を書きました(10ヶ月ぶり3記事目)、ナガクラ(@nagakuta)です。

今回はリハビリも兼ねて、RxSwiftRealmを組み合わせた小技を紹介します!

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で検知してからのハンドリング実装が行いやすい、というのが、僕がRealmRxSwiftを組み合わせる利点だなと思っています。

どうやって実装するの?

実装環境
  • Swift: 4.2
  • Realm: v3.11.1
  • RxSwift: 4.4.0

Realmでのバックグラウンド更新

Realmでバックグラウンド更新をかける方法については、こちらのCookbookを参考にしました。

realm.io

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の更新処理をハンドリングして、いいアプリを作っていきましょう!!!(๑•̀ㅂ•́)و✧