神刀安全网

Building a Simple Swift App With Fine-Grained Notifications

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. In 0.99, we gave them just that power, by deprecating the existing addNotificationBlock API and replacing it with a new one:

func addNotificationBlock(block: (RealmCollectionChange<T>) -> Void) -> NotificationToken

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.objectForPrimaryKey(Repo.self, key: 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 = NSDate(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).sorted("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. :broken_heart:

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, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {         let repo = repos[indexPath.row]          let cell = tableView.dequeueReusableCellWithIdentifier("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! :boom:) 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.insertRowsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) },             withRowAnimation: .Automatic)          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.insertRowsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) },             withRowAnimation: .Automatic)

We convert (or map if you will) insertions from a [Int] to [NSIndexSet] and pass it to insertRowsAtIndexPaths(_:, withRowAnimation:) . 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:

Building a Simple Swift App With Fine-Grained Notifications

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):

Building a Simple Swift App With Fine-Grained Notifications

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.insertRowsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) },     withRowAnimation: .Automatic) tableView.deleteRowsAtIndexPaths(deletions.map { NSIndexPath(forRow: $0, inSection: 0) },     withRowAnimation: .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.objectForPrimaryKey(Repo.self, key: id) {     //update - this is new!     let lastPushDate = NSDate(fromString: pushedAt, format: .ISO8601(.DateTimeSec))     if repo.pushedAt.distanceTo(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 = NSDate(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):

Building a Simple Swift App With Fine-Grained Notifications

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.moveRowAtIndexPath(_:, toIndexPath) 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.reloadRowsAtIndexPaths(_:, withRowAnimation:) 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. :sparkles:

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.animateWithDuration(2.0, animations: {         self.backgroundView!.backgroundColor = UIColor.whiteColor()     }) }

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 = NSIndexPath(forRow: row, inSection: 0)             let cell = tableView.cellForRowAtIndexPath(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:

Building a Simple Swift App With Fine-Grained Notifications

(OK, it was me who starred the repo – but my point remains valid. :grin:)

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 are excited to have released the most demanded Realm feature of all time and we’re looking to your feedback! Tell us what you think on Twitter & GitHub .

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Building a Simple Swift App With Fine-Grained Notifications

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址