I am the author of iOS 17 Fundamentals, Building iOS User Interfaces with SwiftUI, and eight other courses on Pluralsight.
Deepen your understanding by watching!
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:
- First of all, notice that first argument of the majority of the
tableView
methods takes an array ofNSIndexPaths
. Be sure to wrap your argument in[
and]
to create an array. - 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 atindexPath
. Move will require a deletion of theindexPath
and an insertion at thenewIndexPath
. 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.