Earlier in this series, I already mentioned that the NSFetchedResultsController class was designed for table views—and collection views. This also means that it has no problem managing hierarchical data to populate a table view with sections.

In this tutorial, we add sections to the table view of quotes. Each section contains the quotes of a particular author. If you want to follow along, download the source files of this tutorial at the bottom of the tutorial.

Defining a Key Path

The table view currently contains one section. If the number of quotes grows, we want to make it easier for the user to browse the quotes. One solution is to split the table view up into sections, a section for each author.

For that to work, we first need to tell the NSFetchedResultsController instance that it needs to group the quotes it manages by author. This is very easy to do. Revisit the implementation of the fetchedResultsController property and update the initialization of the NSFetchedResultsController instance as shown below.

fileprivate lazy var fetchedResultsController: NSFetchedResultsController<Quote> = {

    ...

    // Create Fetched Results Controller
    let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.persistentContainer.viewContext, sectionNameKeyPath: #keyPath(Quote.author), cacheName: nil)

    ...

    return fetchedResultsController
}()

The value of the third parameter of the initializer defines the key path the fetched results controller uses to split the results of the fetch request up into groups or sections. By passing in #keyPath(Quote.author), we order the fetched results controller to use the value of the author property to split the quotes up into sections.

Updating the Table View

Because the table view can contain multiple sections, we need to let it know how many sections it contains by implementing the numberOfSections(in:) method of the UITableViewDataSource protocol. This is what that looks like.

func numberOfSections(in tableView: UITableView) -> Int {
    guard let sections = fetchedResultsController.sections else { return 0 }
    return sections.count
}

We ask the fetched results controller for its sections property. Because the sections property is an optional type, we return 0 if the fetched results controller doesn't have any sections. If it does, we return the number of sections.

The next method we need to update is the tableView(_:numberOfRowsInSection:) method of the UITableViewDataSource protocol.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    guard let sectionInfo = fetchedResultsController.sections?[section] else { fatalError("Unexpected Section") }
    return sectionInfo.numberOfObjects
}

We ask the NSFetchedResultsController instance for the section that corresponds with the value of the section parameter. The result is an object that conforms to the NSFetchedResultsSectionInfo protocol. This object can tell us the number of quotes present in the section.

A section also needs to have a title, which means we need to implement the tableView(_:titleForHeaderInSection:) method. We request the NSFetchedResultsSectionInfo instance for the section and return the value of its name property. That's it.

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    guard let sectionInfo = fetchedResultsController.sections?[section] else { fatalError("Unexpected Section") }
    return sectionInfo.name
}

The name property is automatically set because we defined during the initialization of the NSFetchedResultsController instance which key path to use to split the managed object up into sections.

let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.persistentContainer.viewContext, sectionNameKeyPath: #keyPath(Quote.author), cacheName: nil)

Updating Sections

There is one detail we have overlooked. What happens if you delete the last quote of a section? And what happens if you change the author of a quote?

We need to update a section if its section information changes. Fortunately, the NSFetchedResultsControllerDelegate protocol has us covered. We only need to implement one additional delegate method, controller(_:didChange:atSectionIndex:for:).

The fetched results controller informs its delegate when a section needs to be inserted into or deleted from the table view. It is the task of the delegate to update the table view or collection view. This is what the implementation looks like.

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
    switch type {
    case .insert:
        tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
    case .delete:
        tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
    default:
        break;
    }
}

The implementation looks similar to that of the controller(_:didChange:at:for:newIndexPath:) method. We insert a section if the change type is equal to insert and we delete a section if the change type is equal to delete. That's it.

Run the application and give it a try. The application can now handle a table view with sections. Inserts, updates, and deletes are handled seamlessly.

Add Sections to Table View With NSFetchedResultsController and Swift

What's Next?

Even though you can build Core Data applications without the NSFetchedResultsController class, I hope this series has convinced you of the power of the NSFetchedResultsController class and the NSFetchedResultsControllerDelegate protocol.

It is possible to keep a table view or collection view up to date without using a fetched results controller, but it requires a lot of code and quite a bit of trial and error. Spare yourself the effort with this robust Core Data component.