Updating Uitableview with Multiple Sections from Rlmresults.Observe()

UITableView with Multiple Sections using Realm and Swift

Here's some sample code that does exactly what you're looking for:

import UIKit
import RealmSwift

class Dog: Object {
dynamic var name = ""
dynamic var race = ""
dynamic var age = 0
dynamic var owner = ""
dynamic var dogID = ""

override static func primaryKey() -> String? {
return "dogID"
}

convenience init(name: String, race: String, dogID: String) {
self.init()
self.name = name
self.race = race
self.dogID = dogID
}
}

class TableViewController: UITableViewController {
let items = try! Realm().objects(Dog.self).sorted(["race", "name"])
var sectionNames: [String] {
return Set(items.valueForKeyPath("race") as! [String]).sort()
}

override func viewDidLoad() {
super.viewDidLoad()

tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")

let realm = try! Realm()
if realm.isEmpty {
try! realm.write {
realm.add(Dog(name: "Bailey", race: "Golden Retrievers", dogID: "0"))
realm.add(Dog(name: "Bella", race: "German Shepherds", dogID: "1"))
realm.add(Dog(name: "Max", race: "Bulldogs", dogID: "2"))
realm.add(Dog(name: "Lucy", race: "Yorkshire Terriers", dogID: "3"))
realm.add(Dog(name: "Charlie", race: "Bulldogs", dogID: "4"))
realm.add(Dog(name: "Molly", race: "German Shepherds", dogID: "5"))
realm.add(Dog(name: "Buddy", race: "German Shepherds", dogID: "6"))
realm.add(Dog(name: "Daisy", race: "Siberian Huskies", dogID: "7"))
}
}
}

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return sectionNames.count
}

override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sectionNames[section]
}

override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
return items.filter("race == %@", sectionNames[section]).count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel?.text = items.filter("race == %@", sectionNames[indexPath.section])[indexPath.row].name
return cell
}
}

Which looks like this:

screenshot

What to do when object on realm gets invalidated


Problem

As I understand, you want to access object's properties when the object is invalidated. Correct me if I'm wrong :)

First of all, let take a look at isInvalidated property

Indicates if the object can no longer be accessed because it is now invalid.

An object can no longer be accessed if the object has been deleted from the Realm that manages it, or if invalidate() is called on that Realm.

It means that an object can be invalidated only when it's managed by a Realm.

My solution

Detach object from the Realm which managers it. If object isn't managed by any Realm, of course it will never be invalidated and you can access properties as you want.

How to do

Whenever you fetch an object from Realm, detach it (create a cloned object).

  1. Add below code to your project. It's used to detach every object in Result after fetching from Realm. I found here

    protocol DetachableObject: AnyObject {
    func detached() -> Self
    }

    extension Object: DetachableObject {

    func detached() -> Self {
    let detached = type(of: self).init()
    for property in objectSchema.properties {
    guard let value = value(forKey: property.name) else { continue }

    if property.isArray == true {
    //Realm List property support
    let detachable = value as? DetachableObject
    detached.setValue(detachable?.detached(), forKey: property.name)
    } else if property.type == .object {
    //Realm Object property support
    let detachable = value as? DetachableObject
    detached.setValue(detachable?.detached(), forKey: property.name)
    } else {
    detached.setValue(value, forKey: property.name)
    }
    }
    return detached
    }
    }

    extension List: DetachableObject {
    func detached() -> List<Element> {
    let result = List<Element>()

    forEach {
    if let detachable = $0 as? DetachableObject {
    let detached = detachable.detached() as! Element
    result.append(detached)
    } else {
    result.append($0) //Primtives are pass by value; don't need to recreate
    }
    }

    return result
    }

    func toArray() -> [Element] {
    return Array(self.detached())
    }
    }

    extension Results {
    func toArray() -> [Element] {
    let result = List<Element>()

    forEach {
    result.append($0)
    }

    return Array(result.detached())
    }
    }
  2. Instead of keep events as RLMResults<RLMMyEvent>, keep it as [RLMMyEvent].

  3. After fetching result, detach objects and convert to an array

    events = RLMMyEvent.objects(with: predicate).toArray()

Now you can access properties without being afraid of invalidated objects and crash.

Note that detached objects will not be updated if the original objects or their values inside Realm are changed.

Display NSProgressIndicator while filtering Realm

This is a perfect use case for Realm's collection notifications. That'd look something like:

var notificationToken: NotificationToken? = nil
var results: Results<MyClass>? = nil

func performFilterAction() {
filterProgressIndicator.isHidden = false
filterProgressIndicator.startAnimation(nil)

let realm = try! Realm()
results = realm.objects(MyClass.self).filter("location = %@", "US")
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()
self.filterProgressIndicator.isHidden = true
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
}
}
}

deinit {
notificationToken?.stop()
}

This lets you display a progress indicator while the initial filtering is performed, and also provides animated updates as changes are made to the data in the collection. If you don't care about the latter you could instead just call reloadData() in the .update case as well.



Related Topics



Leave a reply



Submit