Introduction
NSFetchedResultsController
is a controller, but it’s not a view controller. It has no user interface. Its purpose is to make developers' lives easier by abstracting away much of the code needed to synchronize a table view with a data source backed by Core Data.
NSFetchedResultsController
abstracts away most of the code needed to synchronize a table view with a Core Data store. At its core, NSFetchedResultsController
is a wrapper around an NSFetchRequest
and a container for its fetched results. A fetched results controller can listen for changes in its result set and notify its delegate, NSFetchedResultsControllerDelegate
, to respond to these changes. NSFetchedResultsControllerDelegate
monitors changes in individual Core Data records (whether they were inserted, deleted or modified) as well as changes to entire sections.
Let's create NSFetchedResultsController
.
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } let managedContext = appDelegate.persistentContainer.viewContext lazy var fetchedResultsController: NSFetchedResultsController<Item> = { let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest() let sort = NSSortDescriptor(key: #keyPath(Item.price), ascending: true) fetchRequest.sortDescriptors = [sort] //let predicate = NSPredicate(format: "price == 5") //fetchRequest.predicate = predicate let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedContext, sectionNameKeyPath: nil, cacheName: nil) return fetchedResultsController }()
Like NSFetchRequest
, NSFetchedResultsController
requires a generic type parameter, Item
in this case, to specify the type of entity you expect to be working with.
Next, add the following code to the end of viewDidLoad()
to actually do the fetching:
do { try fetchedResultsController.performFetch() tableView.reloadData() } catch let error as NSError { print("Fetching error: \(error), \(error.userInfo)") }
NSFetchedResultsController
is both a wrapper around a fetch request and a container for its fetched results. You can get them either with the fetchedObjects
property or the object(at:)
method.
Next, you’ll connect the fetched results controller to the usual table view data source methods. The fetched results determine both the number of sections and the number of rows per section.
With this in mind, reimplement numberOfSections(in:)
and tableView(_:numberOfRowsInSection:)
, as shown below:
func numberOfSections(in tableView: UITableView) -> Int { return fetchedResultsController.sections?.count ?? 0 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // return fetchedResultsController.fetchedObjects?.count ?? 0 guard let sectionInfo = fetchedResultsController.sections?[section] else { return 0 } return sectionInfo.numberOfObjects } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let item = fetchedResultsController.object(at: indexPath) item.price += 1 saveContext() tableView.reloadData() }
You will only have one section this time around, but keep in mind that NSFetchedResultsController
can split up your data into sections.
Implementing tableView(_:cellForRowAt:)
would typically be the next step.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") else { return UITableViewCell() } let item = fetchedResultsController.object(at: indexPath) cell.textLabel?.text = item.title return cell }
A quick look at the method, however, reveals it’s already vending ItemCell
cells as necessary. What you need to change is the helper method that populates the cell. Find configure(cell:for:)
and replace it with the following:
Grouping results into sections
In this section, you’ll split up the list of items into their categories. NSFetchedResultsController
makes this very simple. Let’s see it in action. Go back to the lazy
property that instantiates your NSFetchedResultsController
and make the following change to the fetched results controller’s initializer:
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedContext, sectionNameKeyPath: #keyPath(Item.category), cacheName: nil)
The difference here is you’re passing in a value for the optional sectionNameKeyPath
parameter. You can use this parameter to specify an attribute the fetched results controller should use to group the results and generate sections.
How exactly are these sections generated? Each unique attribute value becomes a section. NSFetchedResultsController
then groups its fetched results into these sections.
The fetched results controller will now report the sections and rows to the table view, but the current UI won’t look any different. To fix this problem, add the following method to the UITableViewDataSource
extension:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { let sectionInfo = fetchedResultsController.sections?[section] return sectionInfo?.name }
Monitoring changes
Earlier, when we implemented the tap to increment the price, we added a line of code to reload the table view to show the updated price. This was a brute force solution, but it worked. Sure, you could have reloaded only the selected cell by being smart about the UITableView API, but that wouldn’t have solved the root problem.
NSFetchedResultsController
can listen for changes in its result set and notify its delegate, NSFetchedResultsControllerDelegate
. You can use this delegate to refresh the table view as needed any time the underlying data changes.
Add the following extension to the bottom of the file:
extension ViewController: NSFetchedResultsControllerDelegate { }
This simply tells the compiler the ViewController
class will implement some of the fetched results controller’s delegate methods. Next, go back to your lazy NSFetchedResultsController
property and set the view controller as the fetched results controller’s delegate before returning. Add the following line of code after you initialize the fetched results controller:
fetchedResultsController.delegate = self
First, remove the reloadData()
call from tableView(_:didSelectRowAt:)
.
NSFetchedResultsControllerDelegate
has four methods that come in varying degrees of granularity.
Add the following method inside the NSFetchedResultsControllerDelegate
extension:
//func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { // tableView.reloadData() //} func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.beginUpdates() } func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case .insert: tableView.insertRows(at: [newIndexPath!], with: .automatic) case .delete: tableView.deleteRows(at: [indexPath!], with: .automatic) case .update: let cell = tableView.cellForRow(at: indexPath!) as! ItemCell configure(cell: cell, for: indexPath!) case .move: tableView.deleteRows(at: [indexPath!], with: .automatic) tableView.insertRows(at: [newIndexPath!], with: .automatic) @unknown default: print("Unexpected NSFetchedResultsChangeType") } } func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.endUpdates() }
There is one more NSFetchedResultsControllerDelegate
method to explore. Add it to the extension:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { let indexSet = IndexSet(integer: sectionIndex) switch type { case .insert: tableView.insertSections(indexSet, with: .automatic) case .delete: tableView.deleteSections(indexSet, with: .automatic) default: break } }
This delegate method is similar to controllerDidChangeContent(_:)
but notifies you of changes to sections rather than to individual objects. Here, you handle the cases where changes in the underlying data trigger the creation or deletion of an entire section.