RealmResultsController

What is it?

In a single phrase, we can say that

RealmResultsController is a CoreData’s `NSFetchedResultsController` equivalent for Realm written in Swift

The Redbooth iOS App has a heavy use of `CoreData` and `NSFetchedResultsController`, so when we decided to start using Realm we faced our first problem, there was no equivalent for it. We looked at the existing libraries (like RBQFetchedResultsController) but at the moment it was written in Objc and we wanted something purely Swift 2.0 and with some extra features. At that point, we decided to write our own solution and make it open source!

If you are not familiarized with `NSFetchedResultsController` (Apple Docs), it allows you to efficiently manage the results returned from a Core Data fetch request to provide data for a `UITableView` object. `NSFetchedResultsController` monitors changes in DB objects and notify the view about those changes allowing you to be reactive about them.

We love `Realm`, is faster than `CoreData`, but we think it needed a feature like this to make it even better. `RealmResultsController` does the same for a Realm request with a couple of extra features.

GitHub_Logo

 

How it works

Realm Limitations

There are two important limitations to consider when building an add-on for Realm

  • There aren’t fine-grained notifications, we can’t know easily when an object has changed in a Realm, only when a write transaction finishes.
  • The `Objects` are not thread-safe so our library has to work around that

Solutions

We built a `Realm` proxy that implements the same functions (add, create, delete…) and ends calling the original ones, but in the middle allows us to detect all the operations done in realm objects during a transaction.

You must use these methods so the RRC can listen to all the changes in Realm.

// Basic operations
realm.write {
  realm.addNotififed(myObject, update: true) // add the object or update if    exist
  realm.createNotified(myObject) // create the object in realm
  realm.deleteNotified(myObject) // delete the object from realm
  myObject.notifyChange() // just notify the RRC that the object has change    d in some way
}

 

The library does all the hard work in background, mantaining an updated cache and calculating the indexPaths for each operation. But since `Realm` Objects are not thread safe. Instead of sending `Realm` objects directly to the RRC, we mirror them using `Reflection` in swift, so we end up with an identical object that is `NOT` associated to any realm and therefore it is thread-safe to operate with.

The RRC does all operations in these thread-safe objects, and for the public API, we offer two solutions:

  • When initalizing the RRC, you can pass a `mapper` function (T -> U) from your Realm.Object (T) to another non-realm object (U). If you do so, the RRC delegate methods will return those mapped non-realm objects intead of the original ones.
  • If you don’t pass any mapper, the RRC will just return the mirrored objects (remember they are not actually inside any Realm, i.e. they are thread safe)
//Example initalizing a RRC with a mapper
let rrc = RealmResultsController<TaskModel, Task>(request: request, sectionKeyPath: sectionKeypath, mapper: Task.map, filter: MyFilterFunc)

//Without a mapper
let rrc = RealmResultsController<TaskModel, TaskModel>(request: request, sectionKeyPath: sectionKeypath)

For more details about all the RRC methods, initializers, etc… please take a look to our Repository

For reference, this is a diagram of the RRC structure:

I want to use it in my App!!

Ok, let’s say we have a Realm database with a model called `Task` and a model called `Project` and we want to show these `Task`s in a `UITableView`, but only the not resolved ones that belong to me.

Also, we want to sort them by Name and organize them in sections depending on which `Project` they belong to.

import RealmSwift

class Task: Object {
  dynamic var id: Int = 0
  dynamic var title: String = "Default"
  dynamic var resolved: Bool = false
  dynamic var projectID: Int = 0
  dynamic var project: Project?

  // For the RRC to work, all the used Objects must have a primaryKey
  override static func primaryKey() -> String? {
    return "id"
  }

  func taskBelongsToMe() -> Bool {
    // Check if the task is mine
  }

  func mapToPOSO() -> TaskPOSO {
    // mapper func
  }
}

class Project: Object {
  dynamic var id: Int
  dynamic var name: String

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

class TaskPOSO {
  // Plain old swift object, thread-safe and ready to use in the view
}

First step: initialize the RRC

First, we need a `RealmRequest` object, it represents what we want to fetch from DB.

// Fetch the not resolved tasks
let predicate = NSPredicate(format: "resolved == 0")

// The first SortDescriptor must be equal to the SectionKeyPath (if any)
// There must be always at least one SortDescriptor
// Sort by "projectID" and then by "name"
let sortDescriptors = [SortDescriptor(property: "projectID"),
SortDescriptor(property: "name")]

// We create the request with the predicate and the sortDescriptors
// indicating in which realm do we want to make the fetch
let request = RealmRequest(predicate: predicate,
realm: myRealm,
sortDescriptors: sortDescriptors)

With the `RealmRequest` we can initialize our `RRC`.
This is an `RRC` that fetches `Task` and maps them to `TaskPOSO` objects

let rrc = RealmResultsController<Task, TaskPOSO>(
  request: request,
  sectionKeyPath: "projectID",
  mapper: Task.mapToPOSO,
  filter: { $0.taskBelongsToMe() }
)

// In case you are wondering what "filter" is:
// NSPredicate is very limited sometimes, you can only use it with Realm stored properties
// With this optional func you can apply an extra filter
// to your objects calling functions or computed properties

There is a delegate protocol that you should conform to in order to receive all the RRC notifications:

rrc.delegate = self

Then, to start receiving updates, we just need to:

rrc.performFetch()

The Delegate methods are almost equal to the `NSFetchResultsController` ones and you will receive updates the same way you did with `NSFetchResultsController`:

// You must implement them all

func willChangeResults(controller: AnyObject)
func didChangeObject(controller: AnyObject, object: U, oldIndexPath: NSIndexPath, newIndexPath: NSIndexPath, changeType: RealmResultsChangeType)
func didChangeSection(controller: AnyObject, section: RealmSection, index: Int, changeType: RealmResultsChangeType)
func didChangeResults(controller: AnyObject)

A generic implementation could be:

func willChangeResults(controller: AnyObject) {
  tableView.beginUpdates()
}

func didChangeObject(controller: AnyObject, object: U, oldIndexPath: NSIndexPath, newIndexPath: NSIndexPath, changeType: RealmResultsChangeType) {
  switch changeType {
    case .Delete:
      tableView.deleteRowsAtIndexPaths([newIndexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
    case .Insert:
      tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
    case .Move:
      tableView.deleteRowsAtIndexPaths([oldIndexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
      tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
    case .Update:
      tableView.reloadRowsAtIndexPaths([newIndexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
   }
}

func didChangeSection(controller: AnyObject, section: RealmSection, index: Int, changeType: RealmResultsChangeType) {
  switch changeType {
    case .Delete:
      tableView.deleteSections(NSIndexSet(index: index), withRowAnimation: UITableViewRowAnimation.Automatic)
    case .Insert:
      tableView.insertSections(NSIndexSet(index: index), withRowAnimation: UITableViewRowAnimation.Automatic)
    default:
      break
  }
}

func didChangeResults(controller: AnyObject) {
  tableView.endUpdates()
}

At this point we have the `RRC` fully configured and ready to use, but we need it to integrate it with the `UITableViewDataSource` methods:

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
  return rrc.numberOfSections
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  return rrc.numberOfObjectsAt(section)
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  var cell = tableView.dequeueReusableCellWithIdentifier("taskCell")
  if cell == nil {
    cell = UITableViewCell(style: .Default, reuseIdentifier: "taskCell")
  }
  // This is the correct way to access an object fetched by the RRC
  // This object will be of the Type specified when the RRC was created
  let task = rrc.objectAt(indexPath) // task is a TaskPOSO Object

  cell?.textLabel?.text = task.name + " :: " + String(task.projectID)
  return cell!
}

Now we are settled!!
If we create a new Task that meets the requirements, modify or delete a existing one, we will be notified.

// Create new object
realm.write {
  let newTask = Task()
  newTask.id = 123
  newTask.name = "1. first task"
  newTask.projectID = 1
  realm.addNotified(newTask)
}

// Update a existing one
realm.write {
  let task = realm.objectForPrimaryKey(Task, key: 123)
  task.name = "2. new name!"
  task.notifyChange()
}

// Delete existing object
realm.write {
  let task = realm.objectForPrimaryKey(Task, key: 123)
  realm.deleteNotified(task)
}

For a more detailed working example, check out the RealmResultsController Repository

 

Conclusion

We’ve been really happy working with Realm, and now that we have an alternative to `NSFetchResultsController` even more. `RealmResultsController` aims to extend and improve Realm adding a feature that is very well known for `CoreData` users.

It also adds a couple of features (mapping and filtering) that weren’t in the original controller but we think that they can be really useful to make better and more explicit request to Realm. Also, adding a mapping feature allows us to separate the view from the model, we don’t receive models at the view level.

`RealmResultsController` can help you be more reactive in your views to model changes in your database without using pure reactive programming.

It is 100% covered with tests, thread-safe and a reliable library that we use in our own app. We’ll be happy to receive all the feedback you have about it, if you detect a bug or have a feature request, please fill an Issue in our Repository or just send a PR directly 🙂

GitHub_Logo

 

Isaac Roldán

In a flying trapeze inside a Tardis - iOS Developer at @redboothHQ

 

Leave a Reply

Your email address will not be published. Required fields are marked *