[iOS] Realmに追加された待望の新機能!通知のスキップについて。


Realm Advent Calendar 2016 6日目です。

Realm, RealmSwiftの2.1のアップデートで、通知をスキップする書き込みが追加されました。
Realmの通知はデータベースに対する変更が起きた場合に必ず呼ばれ、基本的にはUI更新は通知内で行いますが、この方法にはいくつか問題が発生するケースがあります。例えばセルの並び替えなどユーザ主導のアクションによりデータベースに変更が加えられた場合に、通知内のUI更新が複雑になってしまうことがあります。

通知をスキップすることにより、これらの問題をよりシンプルに解決することができるようになりました。

サンプル

この記事の説明に使用しているサンプルです。
+ボタンでセルを追加、Editボタンで削除と並び替えを行えます。

通知で制御しづらいケース

具体例として、UITableViewでセルの移動時に起きるUI更新の問題について取り上げます。

モデルオブジェクトが更新された時のUI更新は次の通りです。
通知のchangesdeletions, insertions, modificationsを持っているので、セルの挿入、削除、更新は通知内で簡単に対応することができます。

変更通知からのUI更新
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を更新する必要があります。

通知内で編集中フラグを確認し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を指定することで、その通知を呼ばなくすることができます。

セルの移動時の通知のスキップとUIの更新
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の更新に限らずより柔軟な通知の制御が可能となりました。