Realm Advent Calendar 2016 6日目です。
Realm, RealmSwiftの2.1のアップデートで、通知をスキップする書き込みが追加されました。
Realmの通知はデータベースに対する変更が起きた場合に必ず呼ばれ、基本的にはUI更新は通知内で行いますが、この方法にはいくつか問題が発生するケースがあります。例えばセルの並び替えなどユーザ主導のアクションによりデータベースに変更が加えられた場合に、通知内のUI更新が複雑になってしまうことがあります。
通知をスキップすることにより、これらの問題をよりシンプルに解決することができるようになりました。
サンプル
この記事の説明に使用しているサンプルです。
+ボタン
でセルを追加、Editボタン
で削除と並び替えを行えます。
通知で制御しづらいケース
具体例として、UITableViewでセルの移動時に起きるUI更新の問題について取り上げます。
モデルオブジェクトが更新された時のUI更新は次の通りです。
通知のchanges
がdeletions
, insertions
, modifications
を持っているので、セルの挿入、削除、更新は通知内で簡単に対応することができます。
notificationToken = list.objects.addNotificationBlock { [weak self] (changes) in
guard let strongSelf = self,
strongSelf.isViewLoaded,
let tableView = strongSelf.tableView else { return }
switch changes {
case .initial:
break
case .update(_, let deletions, let insertions, let modifications):
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.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):
fatalError("\(error)")
break
}
}
セルを移動する場合はどうでしょうか。
セルの移動は移動元と移動先のIndexPathがあり、これらの情報はRealmの通知には含まれていません。適切なUI更新を行うためには通知内のUI更新をスキップし、別途IndexPathを使いUIを更新する必要があります。
notificationToken = list.objects.addNotificationBlock { [weak self] (changes) in
guard let strongSelf = self,
strongSelf.isViewLoaded,
let tableView = strongSelf.tableView else { return }
if strongSelf.isEditing { return } // 編集中はUIの更新を行わない。
/* 通常のUI更新 */
}
上記の例は単純にisEditing
のみを確認してますが、これは他にも編集中の追加や削除についても考える必要があります。
問題となるのは、Realmの通知はどのような変更でも呼ばれることになるので、場合によってはUI更新の制御が複雑になってしまうことです。
通知のスキップ
Realmのコミット関数に新しくcommitWrite(withoutNotifying:)
が追加されました。
引数にスキップしたいNotificationToken
を指定することで、その通知を呼ばなくすることができます。
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let realm = try! Realm()
realm.beginWrite()
list.objects.move(from: sourceIndexPath.row, to: destinationIndexPath.row) // モデルオブジェクトの移動
try! realm.commitWrite(withoutNotifying: [notificationToken!]) // 指定の通知をスキップしコミット
tableView.moveRow(at: sourceIndexPath, to: destinationIndexPath) // UI更新
}
通知をスキップすることにより、UI更新を通知内で行うか、別途行うかを簡単に制御できるようになりました。
応用
いい設計だなと思ったところは、commitWrite(withoutNotifying:)
がNotificationToken
の配列を受け取るというところです。Realmの通知はRealm
, Results
, List
, LinkingObject
クラスが対応しており、柔軟にオブジェクトの変更通知を受け取ることができます。NotificationToken
の配列を渡せるようになってるおかげで、これらの通知が複数存在する場合に、スキップしたい通知を自由に選択することができます。
今回の例ではUI更新について取り上げましたが、通知内のUI更新に限らずより細かい通知の制御が可能になったところがポイントとなります。
まとめ
- Realmのコミット関数に新しく
Realm.commitWrite(withoutNotifying:)
が追加され、指定の通知をスキップできるようになりました。 - 通知をスキップすることにより
RealmCollectionChange
では対応が難しいUIの更新をより簡単に制御できるようになりました。 -
Realm.commitWrite(withoutNotifying:)
はNotificationToken
の配列を渡すことができ、UIの更新に限らずより柔軟な通知の制御が可能となりました。