Luis Luis - 1 month ago 26
Swift Question

Add Notification to Array of Realm Results

Just started using Realm for an iOS project and I've looked at the example and docs, but can't seem to figure out how to get fine grained notifications for an array of Realm results.

For example, if its just a Results object you can do this

// Observe Results Notifications
notificationToken = results.addNotificationBlock { [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()
break
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
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()
break
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
break
}
}


and this works just fine for a normal tableview with no sections as it just inserts the new cell into section 0.

However, taking a look at the GroupedTableView (tableview with some sections) example they simply add a notification block to the Realm object itself. Which notifies you of any change, not particular insertions/deletions etc.

Like so:

// Set realm notification block
notificationToken = realm.addNotificationBlock { [unowned self] note, realm in
self.tableView.reloadData()
}


While this works, it really isn't the best solution since you lose the nice animations provided by iOS for free.

My question is really just, how could I add fine grained notifications to an array of Results

var objectsBySection = [Results<DemoObject>]()


I've thought about looping through the array and adding a notification block to each result object, however since new Result objects can be added to this 2D array, this doesn't seem like a good solution.

Does anyone have experience using Realm with a sectioned tableview that has dynamically growing amount of sections/cells?

~~~~~~~~~~~~~~~~~~~~~~~~~~ UPDATE WITH ANSWER ~~~~~~~~~~~~~~~~~~~~~

So thanks to @bogdanf, I was able to find a solution to this. I am posting my solution here since it is not exactly the same as @bogdanf suggested, but his answer led me to the solution so here it is.

First, in my application sections are not exactly infinite. The user when adding objects adds onto them, but they're limited quantities. I.e I can create an array and append my actual Realm objects to them, thus allowing me to group the objects via their appropriate section.

So thats the first step, I create an array of all my sections, in my app this amounted to ~48 sections, so the runtime won't be too bad when adding notifications.

After creating my array of sections, I query realm for the correct object that corresponds to the sections like so:

func initObjectsBySection() {
print("Initializing")
for (index, section) in sections.enumerated() {
let unsorted = realm.objects(Object.self).filter("section == %@" , section)
let sorted = unsorted.sorted(byProperty: "year", ascending: false)
objectsBySection.append(sorted)
registerNotification(for: objectsBySection[index], in: index)
}
}


And
registerNotifcation
is what bogdanf suggested, with some changes:

func registerNotification(for objects: Results<Object>, in section: Int) {
let token = objects.addNotificationBlock { [unowned self] (changes: RealmCollectionChange) in
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
self.tableView.reloadData()
break
case .update:
// Query results have changed, so apply them to the UITableView
self.tableView.beginUpdates()
self.tableView.reloadSections(IndexSet.init(integer: section), with: .automatic)
self.tableView.endUpdates()
break
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
break
}

}
notifTokens.append(token)
}


The reason I am just simply reloading sections instead of deleting or inserting specific rows, is because this does the same thing, it's more concise and a crucial factor is it allows for the section title/height to be recalculated.

Since I am starting with an array of sections ~48, this would mean that if the user were to start from a new install, their would be 48 empty sections and that looks horrible.

Instead, what I do is just set the header for the section to 0, i.e:

override func numberOfSections(in tableView: UITableView) -> Int {
return objectsBySection.count
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objectsBySection[section].count
}

override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return objectsBySection[section].count > 0 ? 44 : 0
}


And that's basically it.

Answer

I would go like this, looping through the array as you suggested:

var objectsBySection = [Results<DemoObject>]()

// Fill the objectsBySection array like in your example
...


for (index, objects) in objectsBySection.enumerated() {
    registerNotifications(for: objects, in: index)
}

where the registerNotifications(for:in:) method is defined like this:

func registerNotifications(for results: Results<DemoObject>, in section:Int) {

    let notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
        guard let tableView = self?.tableView else { return }

        switch changes {
        ...
        case .update(_, let deletions, let insertions, let modifications):
            // Query results have changed, so apply them to the UITableView
            tableView.beginUpdates()
            tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: section) }), with: .automatic)
            tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: section)}), with: .automatic)
            tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: section) }), with: .automatic)
            tableView.endUpdates()
            break
        ...
        }
    }
    notificationTokens.append(notificationToken)
}

We assume var notificationTokens is defined at the class level.

Now, you mentioned that new sections could be added at any time, so let's deal with that too. So we add the plain old non-fine grained notification block and we check if there are new sections added.

notificationToken = realm.addNotificationBlock { [unowned self] note, realm in
    // Let's see what the section list looks like now
    let sections = Set(realm.objects(DemoObject.self).value(forKey: "sectionTitle") as! [String])

    if !Set(sectionTitles).isSuperset(of: sections) {
        sectionTitles = Array(sections)

        self.tableView.reloadData()
    }
}

So, in my simplistic approach, it reloads everything only if new sections are added. If you want to benefit from the nice insert animations you could check instead what sections were added, insert them one by one in the table and then add the new objects to them.

Note: My method to check if sections were added is quite intensive, basically it iterates through all the objects in the DB, so you may want to check it with a realistic load in your app. Unfortunately, until Realm would permit distinct or group by queries this is the only way I could imagine to solve this issue.