Building a Simple Swift App With Fine-Grained Notifications

This is a Swift 3 and Realm 2.1 update to the original article about Realm Notifications. The code and text are up to date, you’re welcome!

What are Fine-Grained Notifications?

Prior to Realm Objective-C & Swift 0.99, you could observe for changes on your Results, List, or AnyRealmCollection types by adding a notification block. Any time that any of the data you were watching changed, you would get notified and could trigger an update to your UI.

A lot of people asked for more precise information about what exactly changed in the underlying data so they could implement more flexible updates to their app’s UI. The new API provides information not only about the change in general, but also about the precise indexes that have been inserted into the data set, deleted from it, or modified.

The new API takes a closure which takes a RealmCollectionChange. This closure will be called whenever the data you are interested in changes. You can read more about using this new method in our docs on Collection Notifications, or simply follow this tutorial through for a practical example!

Building a GitHub Repository List App

In this post we’re going to look into creating a small app that shows all the GitHub repositories for a given user. The app will periodically ping GitHub’s JSON API and fetch the latest repo data, like the amount of stars and the date of the latest push.

If you want to dig through the complete app’s source code as you read this post, go ahead and clone the project.

The app is quite simple and consists of two main classes - one called GitHubAPI, which periodically fetches the latest data from GitHub, and the other is the app’s only view controller, which displays the repos in a table view.

Naturally, we’ll start by designing a Repo model class in order to be able to persist repositories in the app’s Realm:

import RealmSwift

class Repo: Object {
    //MARK: properties
    dynamic var name = ""
    dynamic var id: Int = 0
    dynamic var stars = 0
    dynamic var pushedAt: NSTimeInterval = 0

    //MARK: meta
    override class func primaryKey() -> String? { return "id" }
}

The class stores four properties: the repo name, the number of stars, the date of the last push, and, last but not least, the repo’s id, which is the primary key for the Repo class.

GitHubAPI will periodically re-fetch the user’s repos from the JSON API. The code would loop over all JSON objects and for each object will check if the id already exists in the current Realm and update or insert the repo accordingly:

if let repo = realm.object(ofType: Repo.self, forPrimaryKey: id) {
    //update - we'll add this later
} else {
    //insert values fetched from JSON
    let repo = Repo()
    repo.name = name
    repo.stars = stars
    repo.id = id
    repo.pushedAt = Date(fromString: pushedAt, format: .iso8601(.DateTimeSec)).timeIntervalSinceReferenceDate
    realm.add(repo)
}

This piece of code inserts all new repos that GitHubAPI fetches from the web into the app’s Realm.

Next we’ll need to show all Repo objects in a table view. We’ll add a Results<Repo> property to ViewController:

let repos: Results<Repo> = {
  let realm = try! Realm()
  return realm.objects(Repo.self).sorted(byProperty: "pushedAt", ascending: false)
}()
var token: NotificationToken?

repos defines a result set of all Repo objects sorted by their pushedAt property, effectively ordering them from the most recently updated repo to the one getting the least love. 💔

The view controller will need to implement the basic table view data source methods, but those are straightforward so we won’t go into any details:

extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return repos.count
  }
    
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let repo = repos[indexPath.row]
        
    let cell = tableView.dequeueReusableCell(withIdentifier: "RepoCell") as! RepoCell
    cell.configureWith(repo)
    return cell
  }
}

Inserting New Repos

Next, we’ll need to react to updates: In viewDidLoad() we’ll add a notification block to repos, using the new (bam! 💥) fine-grained notifications:

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

  switch changes {
  case .initial:
    tableView.reloadData()
    break
  case .update(let results, let deletions, let insertions, let modifications):

    tableView.beginUpdates()

    //re-order repos when new pushes happen
    tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) },
                         with: .automatic)
    tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) },
                         with: .automatic)

    //flash cells when repo gets more stars
    for row in modifications {
      let indexPath = IndexPath(row: row, section: 0)
      let repo = results[indexPath.row]
      let cell = tableView.cellForRow(at: indexPath) as! RepoCell
      cell.configureWith(repo)
    }

    tableView.endUpdates()
    break
  case .error(let error):
    print(error)
    break
  }
}

This is quite a long piece of code so let’s look what’s happening in there. We add a notification block to repos and create a local constant tableView to allows us to work with the controller’s table view.

The key to making the most of fine-grained notifications is the changes parameter that you get in your notification block. It is a RealmCollectionChange enumeration and there are three different values:

  • .initial(let result) - This is the very first time the block is called; it’s the initial data you get from your Results, List, etc. It does not contain information about any updates, because you still don’t have previous state - in a sense all the data has just been “inserted”. In the example above, we don’t need to use the Results object itself - instead we simply call tableView.reloadData() to make sure the table view shows what we need.
  • .update(let result, let insertions, let deletions, let updates) - This is the case you get each time after the initial call. The last three parameters are [Int], arrays of integers, which tell you which indexes in the data set have been inserted, deleted, or modified.
  • .error(let error) - This is everyone’s least favorite case - something went wrong when refreshing the data set.

Since we’re looking into how to handle fine-grained notifications, we are interested in the line that goes over insertions and adds the corresponding rows into the table view:

tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) },
  with: .automatic)

We convert (or map if you will) insertions from a [Int] to [NSIndexSet] and pass it to insertRows(at:, with:). That’s all it takes to have the table view update with a nice animation!

When we run the app for the very first time it will fall on the .initial case, but since there won’t be any Repo objects yet (because we haven’t fetched anything yet), tableView.reloadData() will not do anything visible on screen.

Each time you start the app after the very first time, there will be stored Repo objects, so initially the app will show the existing data and will update it with the latest values when it fetches the latest JSON from the web.

When the GitHubAPI fetches the user’s repos from the API, the notification block will be called again and this time insertions will contain all the indexes where repos were inserted much like so:

[0, 1, 2, 3, 4, 5, 6, etc.]

The table view will display all inserted rows with a nice animation:

Inserting all repos

That’s neat, right? And since GitHubAPI is periodically fetching the latest data, when the user creates a new repo it will pop up shortly in the table view like so (i.e., it comes as another insertion update when it’s saved into the app’s Realm):

Adding a repo

Re-ordering the list as new data comes in

repos is ordered by pushedAt, so any time the user pushes to any of their repositories that particular repo will move to the top of the table view.

When the order of the data set elements changes the notification block will get called with both insertions and deletions indexes:

insertions = [0]
deletions = [5]

What happened in the example above is that the element that used to be at position 5 (don’t forget the repos are ordered by their last push date) moved to position 0. This means we will have to update the table view code to handle both insertions and deletions:

tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) },
  with: .automatic)
tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) },
  with: .automatic)

Do you see a pattern here? The parameters you get in the .update case suit perfectly the UITableView API. #notacoincidence

With code to handle both insertions and deletions in place, we only need to look into updating the stored repos with the latest JSON data and reflect the changes in the UI.

Back in GitHubAPI, we will need our code to update or insert depending on whether a repo with the given id already exists. The initial code that we had turns into:

if let repo = realm.object(ofType: Repo.self, forPrimaryKey: id) {
  //update - this is new!
  let lastPushDate = Date(fromString: pushedAt, format: .iso8601(.DateTimeSec))
  if repo.pushedAt.distance(to: lastPushDate.timeIntervalSinceReferenceDate) > 1e-16 {
      repo.pushedAt = lastPushDate.timeIntervalSinceReferenceDate
  }
  if repo.stars != stars {
      repo.stars = stars
  }
} else {
  //insert - we had this before
  let repo = Repo()
  repo.name = name
  repo.stars = stars
  repo.id = id
  repo.pushedAt = Date(fromString: pushedAt, 
    format: .iso8601(.DateTimeSec)).timeIntervalSinceReferenceDate
  realm.add(repo)  
}

This code checks if pushedAt is newer in the received JSON data than the date we have in Realm and if so, updates the pushed date on the stored repo.

(It also checks if the star count changed and updates accordingly the repo. We’ll use this info in the next section.)

Now, any time the user pushes to one of their repositories on GitHub, the app will re-order the list accordingly (watch the jazzy repo below):

Updating a repo

You can do the re-ordering in a more interesting way in certain cases. If you are sure that a certain pair of insert and delete indexes is actually an object being moved across the data set look into UITableView.moveRow(at:, to:) for an even nicer move animation.

Refreshing table cells for updated items

If you are well-versed with the UITableView API, you probably already guessed that we could simply pass the modifications array to UITableView.reloadRows(at:, with:) and have the table view refresh rows that have been updated.

However… that’s too easy. Let’s spice it up a notch and write some custom update code!

When the star count on a repo changes the list will not re-order, thus it will be difficult for the user to notice the change. Let’s add a smooth flash animation on the row that got some stargazer love, to attract the user’s attention. ✨

In our custom cell class we’ll need a new method:

func flashBackground() {
  backgroundView = UIView()
  backgroundView!.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 0.7, alpha: 1.0)
  UIView.animate(withDuration: 2.0, animations: {
     self.backgroundView!.backgroundColor = UIColor.white
  })
}

That new method replaces the cell background view with a bright yellow view and then tints it slowly to white.

Let’s call that new method on any cells that need to display updated star count. Back in the view controller we’ll add under the .Update case:

... //initial case up here 
    case .update(let results, let deletions, let insertions, let modifications):
        ... //insert & delete rows
        for row in modifications {
            let indexPath = IndexPath(row: row, section: 0)
            let cell = tableView.cellForRow(at: indexPath) as! RepoCell
            let repo = results[indexPath.row]
            cell.configureWith(repo)
            cell.flashBackground()
        }
        break
    ... //error case down here

We simply loop over the modifications array and build the corresponding table index paths to get each cell that needs to refresh its UI.

We fetch the Repo object from the updated results and pass it into the configureWith(_:) method on the cell (which just updates the text of the cell labels). Finally we call flashBackground() on the cell to trigger the tint animation.

Oh hey - somebody starred one of those repos as I was writing this post:

Custom update a cell

(OK, it was me who starred the repo - but my point remains valid. 😁)

Conclusion

As you can see, building a table view that reacts to changes in your Realm is pretty simple. With fine-grained notifications, you don’t have to reload the whole table view each time. You can simply use the built-in table APIs to trigger single updates as you please.

Keep in mind that Results, List, and other Realm collections are designed to observe changes for a list of a single type of objects. If you’d like to try building a table view with fine-grained notifications with more than one section you might run into complex cases when you will need to batch the notifications so that you can update the table with a single call to beginUpdates() and endUpdates().

If you want to give the app from this post a test drive, you can clone it from this repo.

We’re super excited to have released the most demanded Realm feature of all time! You can read more in the Swift Docs, and show us what you built on Twitter.


Marin Todorov

Marin Todorov

Marin Todorov is an independent iOS consultant and publisher. He’s co-author on the book "RxSwift: Reactive programming with Swift" the author of “iOS Animations by Tutorials”. He's part of Realm and raywenderlich.com. Besides crafting code, Marin also enjoys blogging, writing books, teaching, and speaking. He sometimes open sources his code. He walked the way to Santiago.