Active Filters: Realm

In Bite #231, we took a look at Realm's new Fine-grained notifications functionality. Today we'll build upon this by checking out another new Realm feature: Queryable, Live Inverse Collections. Whew! That's a fancy name. This feature deserves one though, let's check it out.

Here's a Realm object with a relationship defined on it:

class Person: Object {
  dynamic var name = ""
  let dogs = List<Dog>()
}

That dogs property can be used in a query, and it will even stay updated to reflect changes to the property's value made elsewhere in our app, automatically.

None of that is new though. What is new is the inverse of this mechanic.

Meet the all-new LinkingObjects:

class Dog: Object {
  dynamic var name = ""
  dynamic var age = 0
  let owners = LinkingObjects(
    fromType: Person.self, 
    property: "dogs"
  )
}

Here's what we get for defining things this way:

LinkingObjects are live and auto-updating. When new relationships are formed or removed, they will update to reflect the new state.

LinkingObjects can be used In queries, natively. (Previously this would need to be done in our code):

// People that have a child that have a parent named Diane.
realm.objects(Person).filter("ANY children.parents.name == 'Diane'")

// People whose parents have an average age of > 65.
realm.objects(Person).filter("parents.@avg.age > 65")

LinkingObjects behave like regular Realm collections:

// Which of my parents are over the age of 56?
self.parents.filter("age > 56")

  // Calculate the age of my parents.
self.parents.average("age")

More info about Realm can be found at realm.io

We first looked at Realm way back in Bite #49. It's a great data storage solution for our mobile apps. Today we'll start looking at some of the latest improvements in Realm and the new capabilities they offer. First up is Fine-grained notifications. Let's dive in:

Realm has offered notifications of write operations for a while, they look like this:

let token = realm.addNotificationBlock { notif, realm in
  // TODO: viewController.updateUI()
}

These are still around and work great, but it might help to know more about what changed. That's where the new Collection Notifications come in.

Collection notifications give us access the changes that just occurred at a fine-grained level, including the specific indices of insertions, deletions, etc

let results = try! Realm().objects(Spaceship).sorted("name")
let token = results.addNotificationBlock { (changes: RealmCollectionChange) in
  // TODO: self.processChanges(changes)
}

changes here is an enum that looks like this:

public enum RealmCollectionChange<T> {
  case Initial(T)
  case Update(T, deletions: [Int], insertions: [Int], modifications: [Int])
  case Error(NSError)
}

.Update's values can be easily mapped to NSIndexPath objects suitable for use in table views and collection views.

Here's a complete example showing all of this in action:

class SpaceshipsViewController: UITableViewController {
  var notificationToken: NotificationToken? = nil

  override func viewDidLoad() {
    super.viewDidLoad()
    let realm = try! Realm()
    let results = realm.objects(Spaceships).filter("maxSpeed > 0")

    // 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.insertRowsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) }, withRowAnimation: .Automatic)
        tableView.deleteRowsAtIndexPaths(deletions.map { NSIndexPath(forRow: $0, inSection: 0) }, withRowAnimation: .Automatic)
        tableView.reloadRowsAtIndexPaths(modifications.map { NSIndexPath(forRow: $0, inSection: 0) }, withRowAnimation: .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()
  }
}

More info about Realm can be found at realm.io

Topics

#49: Realm Basics 🔮

Topics

Realm is a database made specifically for running on mobile devices. It works on Mac and iOS devices. (It even supports Android!) It's a great alternative to Core Data or even raw SQLite.

There's plenty to love about Realm, but the best part is it's ultra-simple API:

Define an Object

import RealmSwift

class Spaceship: Object {
  dynamic var name = ""
  dynamic var topSpeed = 0 // in km
}

Define a Related Object

class Spaceship: Object {
  dynamic var name = ""
  dynamic var topSpeed = 0
  dynamic var owner: User?
}

class User: Object {
  dynamic var firstName = ""
  dynamic var lastName = ""
  let spaceships = List<Spaceship>()
}

Create an Instance of an Object

var ship = Spaceship()

ship.name = "Outrider"
ship.topSpeed = 1150

var dash = User()

dash.firstName = "Dash"
dash.lastName = "Rendar"

ship.owner = dash

// need one Realm per thread
let realm = Realm()

realm.write {
  realm.add(ship)
}

Find Objects

Queries in Realm couldn't be simpler. They are chainable. You can add as many calls to .filter as you'd like. You can sort results using the chainable sorted function.

realm
  .objects(Spaceship)
  .filter("topSpeed > 1000")
  .filter("name BEGINSWITH 'O'")
  .sorted("topSpeed", ascending: false)

Performance

Realm uses a "zero-copy" design that allows it process > 35 queries a second, as well as insert > 95,000 records a second in a single transaction.

More info about Realm can be found at realm.io.