はじめに
現在開発しているiosアプリでは、データベースにRealmを使っています。
アプリのUIの状態をRealmの状態に応じて変更したい、と思うことがあったので、それを実現するためにNotificationという機能について調べました。
ドキュメントが英語で、読むのに時間がかかってしまうのと、文章を読んでいるときに行滑りしてしまうので自分なりに理解できるようにhackmdに備忘録をとったりしています。それをもう少しまとめて記事にしてみたという感じです。
では、早速メモの内容を書いていきます。
Notification
Realmでは、エンティティやRealmに対する通知を受け取るリスナーを登録することができる。
Realm Notificationsは、Realm全体が変更されたときに通知される。
Collection Notificationsは、それぞれのオブジェクトが変更、追加、削除されたときに通知される。
Notificationは、オブジェクトやRealmを参照している変数が通知トークンを保持している限り通知される。変更を検知する登録をしてあるクラスのトークンに対する強い参照をキープした方がいい。なぜなら、通知トークンがメモリからデアロケートされたとき、通知の登録も解除されてしまうから。
Notificationは、基本的にいつも独自に登録されたスレッド上で通知されるようになっている。そして、そのスレッドはrun loopを所持していないといけない。もしnotificationをメインスレッド以外に登録したければ、run loopの設定をして、そのスレッドでrun loopを開始しなければならない。(スレッドがもともと存在していなかった場合)
通知のハンドラは、関係のある書き込み処理がそれぞれ適用されたあとに非同期で呼ばれる。このとき、どのスレッドやプロセスが書き込み処理を行っても問題ない。
notificationはrun loopを使って通知されるので、run loop上の他のアクティビティによって遅延されてしまうかもしれない。
notificationが即座に通知されなかったときは、複数の書き込み処理を一つに合体してもよい。
Realm notifications
通知ハンドラは、Realm全体に対して登録することもできる。ハンドラの登録されたRealmで書き込み処理が実行されるたびに、通知ハンドラは発火される。(このとき、どのスレッドやプロセスで書き込み処理が行われても大丈夫)
// Observe Realm Notifications
let token = realm.observe { notification, realm in
viewController.updateUI()
}
// later
token.invalidate()
通知ハンドラを発火させずに書き込み処理がしたいときも、Realmは対応できる。
アプリをユーザーのアクションに対して敏感にしたければ、Realmを使えばUIをユーザーのアクションに対してすぐに反応させることができる。通知ハンドラをすでにアップデートされたUIに対して適応させたいときは、データソースとの連携を切った状態にUIを置くことができる。
特定の書き込みに対する特定の通知をオフにしたいときは、トークンをRealm.write(withoutNotifying:)
メソッドに渡してあげる必要がある。
let realm = try! Realm()
// Observe Realm Notifications
let token = realm.observe { notification, realm in
// ... handle update
}
// Later, pass the token in an array to the realm.write(withoutNotifying:)
// method to write without send a notification to that observer.
try! realm.write(withoutNotifying: [token]) {
// ... write to realm
}
// Finally
token.invalidate()
Collection notifications
Collection notificationsはRealm全体を相手にはせず、代わりにもう少しキメの細かい変更の通知を受け取ることができる。これは、最後の通知から追加、削除、修正されたオブジェクト群から構成される。Collection notificationsは非同期で通知される。
これらの変更はRealmCollectionChange
を通じてアクセスすることができる。このオブジェクトは削除、追加、修正に関する情報を所持している。
削除と追加は、コレクションのオブジェクトの開始と停止をいつも記録している。これは、オブジェクトがRealmに追加されたときや、Realmから削除されたときのことを考慮する。特定の値に対してフィルタをかけたりしてオブジェクトが新しくなにかにマッチングしたり、あるいは何にもマッチングしないようになった場合でも、Result
はその結果を反映してくれる。
修正に関する通知は、コレクションのオブジェクトのプロパティが変更されたときに通知される。これは、対1や対多のリレーションでも発生する。しかし、Inverse relationships
(Realmにおいて、フィールドの参照をしかえすやつ)ではこの通知は発生しない。
class Dog: Object {
@objc dynamic var name = ""
@objc dynamic var age = 0
}
class Person: Object {
@objc dynamic var name = ""
let dogs = List<Dog>()
}
この2つのクラスがある状態で、犬の飼い主のリストを監視してみる。
すると、Personオブジェクトの修正に関する通知が来る。
- Personのnameに変更が加えられました。
- PersonのdogsにDogが追加もしくは削除されました。
- Personに従属するDogのageプロパティに変更が加えられました。
これを使えば、通知がくるたびにUI全体を更新するのではなくて個別にアニメーションを操作して視覚的なアップデートをUIに加えることができる。
class ViewController: UITableViewController {
var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let results = realm.objects(Person.self).filter("age > 5")
// Observe Results Notifications
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
// Always apply updates in the following order: deletions, insertions, then modifications.
// Handling insertions before deletions may result in unexpected behavior.
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}
deinit {
notificationToken?.invalidate()
}
}
Object notification
Realmはオブジェクトレベルでの監視もサポートしている。オブジェクトが削除されたり、プロパティのどれかが変更されたりした通知を受け取るたびに特定のRealmのオブジェクトにnotificationを登録することができる。
Realmに管理されているオブジェクトだけは、通知ハンドラをオブジェクト自体に設置することができる。
複数のスレッドや複数のプロセス上で書き込み処理を実行している間は、そのブロックはオブジェクトを管理しているRealmが変更を含んでいるバージョンにリフレッシュされたタイミングで呼ばれる。
今回の通知ハンドラは、ObjectChange
という列挙型の値を使う。その列挙型は、オブジェクトが削除されたかどうか、プロパティの値が変更されたかどうか、もしくはエラーが発生したかどうかを扱う。
例えばオブジェクトが削除されていたら、ObjectChange.deleted
という値で通知される。
このブロックは1度呼び出されたらもう2度と呼び出されない。
オブジェクトのプロパティが変更されていたら、ObjectChange.change
という値が使われる。この値のブロックは、PropertyChange
という値の配列を保持している。この値は変更されたプロパティの名前(文字列型)と、変更前の値と、変更後の値を保持している。
ObjectChange.error
のブロックは、エラーが発生した場合にNSError
を保持して呼び出される。これも2回以上呼び出されない
class StepCounter: Object {
@objc dynamic var steps = 0
}
let stepCounter = StepCounter()
let realm = try! Realm()
try! realm.write {
realm.add(stepCounter)
}
var token : NotificationToken?
token = stepCounter.observe { change in
switch change {
case .change(let properties):
for property in properties {
if property.name == "steps" && property.newValue as! Int > 1000 {
print("Congratulations, you've exceeded 1000 steps.")
token = nil
}
}
case .error(let error):
print("An error occurred: \(error)")
case .deleted:
print("The object was deleted.")
}
}
最後に
あまりこういう勉強は得意じゃなくてなかなか覚えが悪かったりするので、とりあえず実装してリファクタリングを繰り返して、上手に使いこなせるようになっていこうと思います。