Sync Table View Data: NSFetchedResultsController and Swift

Updated on September 23, 2015 – Swift 2.0

My goal with this article is to help you utilize the full power of NSFetchedResultsController.

This is a continuation on a series of articles I’ve written on Core Data and NSFetchedResultsController, so you may want to check out those previous posts to get an idea of where I’m picking up in this walk-through. Previously I touched on how to seed a Core Data database, and how to take that data and display it in a table view with an NSFetchedResultsController.

As with the previous posts, I’m providing an example Xcode project over at GitHub, so feel free to follow along with the live working example:

In this installment to the series, I want to answer the question, “How do I update the rows in a table view when I add or remove objects from the Core Data database?” I will show how to implement the NSFetchedResultsControllerDelegate protocol, which is the key to automatically synchronizing changes made to your Core Data persistent store with a table view.

Examining the NSFetchedResultsControllerDelegate protocol

The NSFetchedResultsControllerDelegate protocol is the piece of the puzzle that helps us update a table view with changes made to the Core Data persistent store. There are five methods that we’ll be taking a look at:

  • controllerWillChangeContent(_:)
  • controller(_:didChangeObject:atIndexPath:forChangeType:newIndexPath:)
  • controller(_:didChangeSection:atIndex:forChangeType:)
  • controller(_:sectionIndexTitleForSectionName:)
  • controllerDidChangeContent(_:)

The two methods that are responsible for doing the actual updates to the table view’s structure are controller(_:didChangeSection:atIndex:forChangeType:) and controller(_:didChangeObject:atIndexPath:forChangeType:newIndexPath:). If some of the changes to the table view result in new sections being created, controller(_:sectionIndexTitleForSectionName:) will help give it an appropriate title (and make sure the other sections keep their appropriate titles as well).

controllerWillChangeContent(_:) and controllerDidChangeContent(_:) help inform the table view that changes are about to happen / just finished happening. Sandwiching the primary “didChangeObject” and “didChangeSection” protocol methods with these two methods allows the table view to animate in all of the changes to its structure in one batch.

So, the general structure of the NSFetchedResultsControllerDelegate section of your source file might look like this:

 1// MARK: NSFetchedResultsControllerDelegate methods
 2public func controllerWillChangeContent(controller: NSFetchedResultsController) {
 3    self.tableView.beginUpdates()
 4}
 5
 6public func controller(
 7    controller: NSFetchedResultsController,
 8    didChangeObject anObject: AnyObject,
 9    atIndexPath indexPath: NSIndexPath?,
10    forChangeType type: NSFetchedResultsChangeType,
11    newIndexPath: NSIndexPath?) {
12        
13        // implementation to follow...
14}
15
16public func controller(
17    controller: NSFetchedResultsController,
18    didChangeSection sectionInfo: NSFetchedResultsSectionInfo,
19    atIndex sectionIndex: Int,
20    forChangeType type: NSFetchedResultsChangeType) {
21    
22        // implementation to follow...
23}
24
25public func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String) -> String? {
26    return sectionName
27}
28
29public func controllerDidChangeContent(controller: NSFetchedResultsController) {
30    self.tableView.endUpdates()
31}

controller(_:didChangeObject:atIndexPath:forChangeType:newIndexPath:)

This is the method that governs how we want to handle the rows in a table view when the synchronization would require inserting rows, updating existing ones, removing them, or reordering them.

I’ll give you the implementation and then point out a couple of “gotchas” and expound a little more. Recall that we’re working with a sample app named “Zootastic”, so if you see references to Animals in the example, you’ll know why. :]

 1public func controller(
 2        controller: NSFetchedResultsController,
 3        didChangeObject anObject: AnyObject,
 4        atIndexPath indexPath: NSIndexPath?,
 5        forChangeType type: NSFetchedResultsChangeType,
 6        newIndexPath: NSIndexPath?) {
 7            
 8            switch type {
 9            case NSFetchedResultsChangeType.Insert:
10                // Note that for Insert, we insert a row at the __newIndexPath__
11                if let insertIndexPath = newIndexPath {
12                    self.tableView.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
13                }
14            case NSFetchedResultsChangeType.Delete:
15                // Note that for Delete, we delete the row at __indexPath__
16                if let deleteIndexPath = indexPath {
17                    self.tableView.deleteRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
18                }
19            case NSFetchedResultsChangeType.Update:
20                if let updateIndexPath = indexPath {
21                    // Note that for Update, we update the row at __indexPath__
22                    let cell = self.tableView.cellForRowAtIndexPath(updateIndexPath)
23                    let animal = self.fetchedResultsController.objectAtIndexPath(updateIndexPath) as? Animal
24                    
25                    cell?.textLabel?.text = animal?.commonName
26                    cell?.detailTextLabel?.text = animal?.habitat
27                }
28            case NSFetchedResultsChangeType.Move:
29                // Note that for Move, we delete the row at __indexPath__
30                if let deleteIndexPath = indexPath {
31                    self.tableView.deleteRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
32                }
33                
34                // Note that for Move, we insert a row at the __newIndexPath__
35                if let insertIndexPath = newIndexPath {
36                    self.tableView.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
37                }
38            }
39}

Right away you’ll notice we enter a switch on the type parameter of the method. There are four options possible in the NSFetchedResultsChangeType enum: Insert, Delete, Update, and Move.

Beware of a few common gotchas with each case of the switch:

  1. First of all, notice that first argument of the majority of the tableView methods takes an array of NSIndexPaths. Be sure to wrap your argument in [ and ] to create an array.
  2. Pay extra attention to which index path parameter you’re referencing in each case. For insert, the goal is to add a row at the newIndexPath. For Delete, the goal is to remove the row at indexPath. Move will require a deletion of the indexPath and an insertion at the newIndexPath. Getting these mixed up will cause runtime errors, so pay close attention here!

controller(_:didChangeSection:atIndex:forChangeType:)

If your table view only has one section, you don’t need to worry with this one.

If your table view has multiple sections, you want to make sure and implement this protocol method – if you fail to do so and the change to the persistent store results in adjustments to the table view that can’t be handled, runtime errors can occur. For example, deleting all rows in a section would result in the section needing to be deleted as well, but without this protocol method being implemented, the update to the table view can’t be made and the app crashes.

Once again, I’ll throw the code your way and follow up with commentary:

 1public func controller(
 2    controller: NSFetchedResultsController,
 3    didChangeSection sectionInfo: NSFetchedResultsSectionInfo,
 4    atIndex sectionIndex: Int,
 5    forChangeType type: NSFetchedResultsChangeType) {
 6    
 7        switch type {
 8        case .Insert:
 9            let sectionIndexSet = NSIndexSet(index: sectionIndex)
10            self.tableView.insertSections(sectionIndexSet, withRowAnimation: UITableViewRowAnimation.Fade)
11        case .Delete:
12            let sectionIndexSet = NSIndexSet(index: sectionIndex)
13            self.tableView.deleteSections(sectionIndexSet, withRowAnimation: UITableViewRowAnimation.Fade)
14        default:
15            ""
16        }
17}
18
19public func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String?) -> String? {
20    return sectionName
21}

For this one, we’re only implementing code for Insert and Delete. The necessary information to insert a section or remove a section (ie, the sectionIndex) comes as a parameter to the method.

We utilize an NSIndexSet to wrap up the section that needs to be inserted or deleted and pass it to the table view’s insertSections() and deleteSections() methods, respectively.

comments powered by Disqus