UI database bindings with Realm & ReactiveKit

UITableView is truly a heart of iOS user interface. It does not matter whether you are changing settings, going through unread emails or sending messages. Provided that you have used your iOS device recently, it is almost certain that you have stumbled upon a table view.

Rendering a table with arbitrary data is a really simple task. You just conform a class to UITableViewDataSource protocol by implementing two required functions. So far, so easy. The next step is to make this table represent some actual data from your app. This is where things are getting complicated. Some typical requirements are:

  1. The data is stored in a local database, filtered and sorted according to a query.
  2. Results of the query can be updated at any time, even by a table itself (marking TODO notes as completed, deleting favorite recipes and so on).
  3. The data is synchronized with a backend service, either on demand or automatically (in the background).

What is more, the points above are not mutually exclusive. In fact, it is typical to have all of them at once in a single view controller. How do you handle UI updates for that? Wiring a synchronizer and a view controller to the notification center seems to do a trick. However, calculating table updates based on a queryset changes is not a trivia. Yes, it is possible to reloadData() everytime, but this won't cut it for marking a single object deleted.

Is there any other option? Yes, it is called NSFetchedResultsController and comes shipped with CoreData. It reports any changes made to a queryset so that you can map them directly to table view updates. Is it convenient to use? Well, just a quick look at the CoreData docs and I am already intimidated by a number of delegate methods that I need to implement. How about you?

You may wonder whether this article is to complain about how much code dedicated to UI updates is needed for every table view. No, it is not. In fact, I am going to show the opposite.

You do not need any dedicated code to handle UI updates for a table view.

What kind of magic is that?

Sample TODO app

Please, bear in mind that this article is up to date as of Xcode 7.3, Swift 2.2 and ReactiveKit v2. It is going to be updated for Swift 3 after it is out of beta.

To illustrate the idea, I have created a sample TODO note application. You can clone its source code from Github. The screencast from the app is presented below.

Sample app

3rd party libraries

The first step for updateless table views is to attach necessary dependencies to a project. To accomplish the result we will need:

  • Realm Swift - a Swift version of a great object database for mobile devices,
  • ReactiveKit - a library that supports reactive programming along with ReactiveUIKit which provides bindings for UIKit controls.

Why Realm? There is at least a couple of different reasons to use Realm. For the project it is the best choice because it can notify about queryset updates similar to CoreData, but the API is much more concise.

Why ReactiveKit and not any other reactive programming library? There is no particular reason other than my familiarity with a framework. You can achieve the same result using RxSwift, ReactiveCocoa or any other (F)RP library that you feel comfortable with.

Carthage

Despite using only CocoaPods for dependency management in the past, I decided to change my approach. This decision was based on two factors:

  • I wanted to give Carthage a try for a long time,
  • the painful experience of using CocoaPods on my previous Swift project.

The problem with CocoaPods is that it rebuilds the whole workspace every time a target is changed or DerivedData/ directory is emptied. Having over 15 dependencies attached and being a victim of frequent Xcode syntax highlighting problems and crashes I spent a considerable amount of time observing a build progress bar. Since it was not entertaining, I have switched to Carthage for the time being.

Installing project dependencies requires a couple of steps. Firstly, you need to create a Cartfile:

# ReactiveKit
github "ReactiveKit/ReactiveKit" ~> 2.1
github "ReactiveKit/ReactiveUIKit" ~> 2.0

# Realm database
github "realm/realm-cocoa" ~> 1.0

Secondly, you need to run carthage update command. It is noticeably slower than pod install counterpart, but it results in much shorter average build time.

After a build process is completed, frameworks need to be attached to a project. Carthage readme documents that really well, so it is a matter of following steps mentioned there.

Reactive programming

ReactiveKit is a library that supports Functional Reactive Programming (FRP). If you are familiar with the term you can skip this section. Otherwise, do not worry. It is there to describe a pretty simple concept of building a software.

Reactive Programming (RP) focuses on a data flow. The idea is that there is an input data that changes constantly and the code should specify how to transform it to an output. Let's assume that there are 5 people reading my blog right now, two of them being female. Or, expressed in a programming manner:

male = 3
female = 2
total = male + female    // 5

Now, a next female hits the page. It changes the input data:

female = 3

In an imperative programming world, a total value would not change. It was calculated once and it will stay this way until it is explicitly changed. In that case total == 5 still evaluates to true, but it certainly is not valid, since I already have six visitors!

If I wanted to show a total number of visitors, reactive paradigm would be a much better fit. The total value (output) would be updated every time that either male or female values (input) are changed.

Functional Reactive Programming (FRP) is an additional layer that describes input to output transformation is done with functional tools (like filter, map or reduce).

Binding UITableView data with ReactiveKit

Back to our application, let's take look at a table view from a reactive perspective. It transforms an array of objects into table cells visible on a screen.

Since we are building a TODO note app, we should start with a trivial TODO note model:

class TODONote {

  var note: String = ""

  init(note: String) {
    self.note = note
  }
}

Then, we need to prototype a table view cell for a list. You can refer to a TODONoteListCell. For the sake of simplicity, we will stick with title only:

class TODONoteListCell: UITableViewCell {

  let titleView = UILabel()
    
  override init(style: UITableViewCellStyle, 
                reuseIdentifier: String?) {
    super.init(style: style, 
               reuseIdentifier: reuseIdentifier)

    self.configureCell()
  }

  private func configureCell() {
    // ... configure cell layout
  }
}

The next step is to build a simple UITableView inside your view controller. It should look along below lines:

class TODONoteListViewController: UIViewController {

  let todoNoteList = UITableView(frame: CGRectZero,
                                 style: .Plain)

  override viewDidLoad() {
    super.viewDidLoad()
    // add todoNoteList to a view and constrain it
    self.list.registerClass(TODONoteListCell.self, 
      forCellReuseIdentifier: "TODONoteListCell")
  }

  // ...
}

It is important to register a cell class for the table. This will make cell configuration much easier in the future.

Now, we have to prepare a data source for a table view, similar to the "usual" way of doing things:

import ReactiveKit

class TODONoteListViewController: UIViewController {
  let notes: CollectionProperty<Array<TODONote>>

  init() {
    let firstNote = TODONote(note: "First note")
    let secondNote = TODONote(note: "Second note")
    self.notes = CollectionProperty([firstNote, secondNote])
  }
}

The only difference is that array of notes is additionally wrapped inside a CollectionProperty. Thanks to CollectionProperty, every time an array is changed, a subscriber gets notified about it. A simple example can be found in ReactiveKit readme.

The final step is to bind a CollectionProperty to a table view:

class TODONoteListViewController: UIViewController {
  override viewDidLoad() {
    // ...
    
    self.notes.bindTo(self.todoNoteList) {
          indexPath, notes, list in
      let note = notes[indexPath.row]
      let cell = list.dequeueReusableCellWithIdentifier(
          "TODONoteListCell") as! TODONoteListCell
      cell.titleView.title = note.note

      return cell
  }
}

Upon running an application you should see a list of two entries. You might have noticed that the code is a little bit shorter than the delegate counterpart. But the biggest advantage of that approach is yet to be discovered. Now, add a new button to the view controller. It can invoke any of the three action types below:

  • insertion (example: notes.append(TODONote(note: "Third"))) ,
  • deletion (example: notes.removeLast()),
  • modification (notes[0].note = "Not a first note anymore").

Test run the application. Yes, that is right. The changes made to a notes array are automatically reflected in a table view now.

Using ReactiveKit with Realm

We are halfway there. Our table view is refreshed automatically, but we still have to wire up a database query to a CollectionProperty. It is high time to switch to an actual implementation of TODONote model:

class TODONote: Object {

    dynamic var guid = ""
    dynamic var note = ""
    dynamic var date = NSDate(timeIntervalSince1970: 1)
    dynamic var completed = false
    private dynamic var priorityRaw = ""
    
    var priority: Priority {
        get {
            return Priority(rawValue: self.priorityRaw)!
        }
        set {
            self.priorityRaw = newValue.rawValue
        }
    }
    
    override static func primaryKey() -> String? {
        return "guid"
    }
}

... and add a couple of notes to a database:

let firstNote = TODONote()
firstNote.guid = "1st"
firstNote.note = "First note"

let secondNote = TODONote()
secondNote.guid = "1st"
secondNote.note = "First note"

let realm = try! Realm()
try! realm.write {
  realm.add(firstNote)
  realm.add(secondNote)
}

Let's fetch all the notes now:

let realm = try! Realm()
let notes: Results<TODONote> = realm.objects(TODONote.self)

As you can see, query results are returned as a Results object:

  • It is a lazy loading collection. Objects are fetched from a database only when they are needed.
  • It is updated automatically to reflect a query state.
  • It conforms to a RealmCollectionType, which derives from CollectionType.

The last point is especially useful for us. Since ReactiveKit's CollectionProperty wraps around CollectionType we can instantiate that with Realm's Results directly:

let notes = CollectionProperty(realm.objects(TODONote.self))

The bad news is that binding such a property directly to a table view would result in a crash or no updates at all. It is because ReactiveKit provides standard protocol extensions for Array type, which triggers an update event upon invoking any method that can modify this array. Such overloads for Results are non-existing in standard ReactiveKit library, since it does not come bundled with Realm.

The good news is that Realm supports collection notifications which is exactly what we need to implement a CollectionProperty. Even better news is that Realm's notifications use almost the same data format to denote collection changes as ReactiveKit does. Having that knowledge, we can easily implement a bridge between the two:

import ReactiveKit
import RealmSwift

class ResultsProperty<C: Object>: CollectionProperty<Results<C>> {

  let results: Results<C>
  var notificationToken: NotificationToken?

  override init(_ results: Results<C>) {
    self.results = results

    super.init(self.results)

    self.startObservingResultsChanges()
  }

  private func startObservingResultsChanges() {
    self.notificationToken = self.results.addNotificationBlock { changes in
        switch changes {
        case .Initial:
          break

        case .Update(_, let deletes, let inserts, let updates):
          let changeset = CollectionChangeset(
            collection: self.results,
            inserts: inserts,
            deletes: deletes,
            updates: updates)

          self.update(changeset)
          break

        case .Error(let error):
          fatalError("\(error)")
          break
        }
    }
  }

  deinit {
    notificationToken?.stop()
  }
}

Realm provides three notification cases that have to be handled in a different ways:

  • .Initial should not trigger any action, since initial value for a CollectionProperty is set through the initializer.
  • .Error should handle any troubles with opening Realm database. Since we fully rely on a database to store a data, there is not much to do here apart from closing an app with a critical exception.
  • .Update provides information about inserted, deleted and modified indices in a collection. We have to remap that to ReactiveKit's CollectionChangeset and invoke update(changeset:) method.

The final step is to attach our brand new ResultsProperty to a table view. We do that by simply replacing notes collection contents:

import ReactiveKit

class TODONoteListViewController: UIViewController {
  let notes: ResultsProperty<TODONote>

  init() {
    let realm = try! Realm()
    let notCompletedNotes = 
        realm.objects(TODONote.self)
          .filter("completed == NO")
          .sorted("date", ascending: true)
    self.notes = ResultsProperty(notCompletedNotes)
  }
}

From now on every change in a database that modifies notCompletedNotes query will automatically trigger UI updates. There are numerous advantages of that approach:

  • There is no need to worry about manually exchanging notifications anymore. It does not matter which action/object triggered the change. Whether it is a button inside the same view controller or a synchronizer class managed in the AppDelegate, all the data changes are handled in exactly the same fashion.
  • There is no need to worry about managing threads. ReactiveKit's data bindings are always updating UI on the main thread, so it does not matter whether the database was modified in a background or not.
  • Single source of truth - the database. There is no need to duplicate data structures and so there is no risk to have any discrepancies between them.

I strongly advise you to try this approach yourself. I was really surprised about how easy and reliable that was.

Please note, that the code presented in snippets is for presentation purposes only and contains shortcuts, like creating notes in init(), that you should never ever use in a production code.

Show Comments