Exploring the Fetched Results Controller Delegate Protocol

Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy

The NSFetchedResultsController class is pretty nice, but you probably aren't convinced yet by what you learned in the previous tutorial. In this tutorial, we explore the NSFetchedResultsControllerDelegate protocol. This protocol enables us to respond to changes in the managed object context the fetched results controller monitors. Let me show you what that means and how it works.

Where We Left Off

In the previous tutorial, we ran into a problem I promised we'd solve in this tutorial. Whenever the user adds a new note, the table view isn't updated with the new note. Visit GitHub to clone or download the project we created in the previous tutorial and open it in Xcode.

git clone https://github.com/bartjacobs/ManagingRecordsWithFetchedResultsControllers

Monitoring a Managed Object Context

Previously, I wrote that a fetched results controller monitors the managed object context it keeps a reference to. It does this to update the results of its fetch request. But how does it monitor the managed object context? How does the fetched results controller know when a record is added, updated, or deleted?

A managed object context broadcasts notifications for three types of events:

  • when one of its managed objects changes
  • when the managed object context is about to save its changes
  • when the managed object context has successfully saved its changes

If an object would like to be notified of these events, they can add themselves as observers for the following notifications:

  • NSNotification.Name.NSManagedObjectContextObjectsDidChange
  • NSNotification.Name.NSManagedObjectContextWillSave
  • NSNotification.Name.NSManagedObjectContextDidSave

With this in mind, the NSFetchedResultsController class is starting to lose some of its magic. A fetched results controller inspects the fetch request it was initialized with and it adds itself as an observer for NSNotification.Name.NSManagedObjectContextObjectsDidChange notifications. This gives the fetched results controller enough information to keep the collection of managed objects it manages updated and synchronized with the state of the managed object context it observes.

You probably know that a notification has a name and, optionally, a userInfo dictionary. Each of the notifications I listed earlier has a userInfo dictionary that contains three key-value pairs. The dictionary includes which managed objects have been:

  • inserted
  • updated
  • deleted

That is how the fetched results controller updates its collection of managed objects. The last piece of the puzzle is the NSFetchedResultsControllerDelegate protocol. The object that manages the fetched results controller, a view controller, for example, doesn't need to inspect the collection of managed objects the fetched results controller manages. It only needs to set itself as the delegate of the fetched results controller.

Let's now take a closer look at the NSFetchedResultsControllerDelegate protocol. The methods of the protocol are:

optional public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
optional public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType)

optional public func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
optional public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)

optional public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, sectionIndexTitleForSectionName sectionName: String) -> String?

The delegate method we are most interested in for this discussion is the first one, controller(_:didChange:at:for:newIndexPath:). This method is invoked every time a managed object is inserted, updated, or deleted. How can we use this method for the Notes application?

Conforming to the Delegate Protocol

Start by conforming the ViewController class to the NSFetchedResultsControllerDelegate protocol using another extension for the ViewController class.

extension ViewController: NSFetchedResultsControllerDelegate {

}

In the fetchedResultsController property, set the view controller as the delegate of the fetched results controller.

fileprivate lazy var fetchedResultsController: NSFetchedResultsController<Note> = {
    // Initialize Fetch Request
    let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()

    // Add Sort Descriptors
    let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: true)
    fetchRequest.sortDescriptors = [sortDescriptor]

    // Initialize Fetched Results Controller
    let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.coreDataManager.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)

    // Configure Fetched Results Controller
    fetchedResultsController.delegate = self

    return fetchedResultsController
}()

The NSFetchedResultsControllerDelegate protocol is an Objective-C protocol. None of the methods of the protocol are required. For the Notes application, the ViewController class needs to implement three methods of the NSFetchedResultsControllerDelegate protocol.

Two methods are used to inform the table view when we're about to make changes and when we've finished making changes. That's what the controllerWillChangeContent(_:) and controllerDidChangeContent(_:) methods are for. The implementation of these methods is straightforward as you can see below.

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.beginUpdates()
}

func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.endUpdates()
}

We also need to implement controller(_:didChange:at:for:newIndexPath:). In this method, we update the table view based on the changes the fetched results controller has detected. The method accepts five arguments:

  • the fetched results controller
  • the managed object that changed
  • the old index path of the managed object
  • the type of change (insert, update, delete, move)
  • the new index path of the managed object
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch (type) {
    case .insert:
        if let indexPath = newIndexPath {
            tableView.insertRows(at: [indexPath], with: .fade)
        }
        break;
    case .delete:
        if let indexPath = indexPath {
            tableView.deleteRows(at: [indexPath], with: .fade)
        }
        break;
    case .update:
        if let indexPath = indexPath, let cell = tableView.cellForRow(at: indexPath) {
            configureCell(cell, at: indexPath)
        }
        break;
    case .move:
        if let indexPath = indexPath {
            tableView.deleteRows(at: [indexPath], with: .fade)
        }

        if let newIndexPath = newIndexPath {
            tableView.insertRows(at: [newIndexPath], with: .fade)
        }
        break;
    }
}

Insert

If a new managed object was inserted into the managed object context, we insert a row into the table view at the corresponding index path. The table view and table view data source handle the details of the insertion.

case .insert:
    if let indexPath = newIndexPath {
        tableView.insertRows(at: [indexPath], with: .fade)
    }
    break;

Delete

If a managed object is deleted and removed from the managed object context, the corresponding row is deleted from the table view.

case .delete:
    if let indexPath = indexPath {
        tableView.deleteRows(at: [indexPath], with: .fade)
    }
    break;

Update

When a managed object is updated, we ask the table view for the corresponding table view cell and invoke a helper method, configureCell(_:at:), to update it. We take a look at the implementation of this helper method in a moment.

case .update:
    if let indexPath = indexPath, let cell = tableView.cellForRow(at: indexPath) {
        configureCell(cell, at: indexPath)
    }
    break;

Move

The collection of managed objects the fetched results controller manages is sorted by the createdAt property. If the order of the collection changes, a move event is triggered. To reflect the new order in the table view, the table view cell that corresponds with the old position of the managed object is deleted and a new table view cell is inserted at the new position of the managed object.

case .move:
    if let indexPath = indexPath {
        tableView.deleteRows(at: [indexPath], with: .fade)
    }

    if let newIndexPath = newIndexPath {
        tableView.insertRows(at: [newIndexPath], with: .fade)
    }
    break;

Implementing the Helper Method

Before we take the application for a spin, we need to implement configureCell(_:at:) and update tableView(_:cellForRowAt:). As you can see, we move part of the implementation of tableView(_:cellForRowAt:) to configureCell(_:at:). The reason for doing this is simple. To update a table view cell, we shouldn't invoke tableView(_:cellForRowAt:). This method should only be invoked by the table view.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "NoteCell", for: indexPath)

    // Configure Cell
    configureCell(cell, at: indexPath)

    return cell
}

func configureCell(_ cell: UITableViewCell, at indexPath: IndexPath) {
    // Fetch Note
    let note = fetchedResultsController.object(at: indexPath)

    // Configure Cell
    cell.textLabel?.text = note.title
    cell.detailTextLabel?.text = note.content
}

Run the application in the simulator or on a physical device to see the result. Every time you add a new note, the table view is updated with the new note.

Exploring the Fetched Results Controller Delegate Protocol | Adding Notes

Updating Notes

To update notes, we need to make a few changes. Start by creating a new UIViewController subclass and name it NoteViewController. The implementation isn't too difficult. The NoteViewController class has two outlets, titleTextField and contentTextView. The class also declares a variable property of type Note?.

import UIKit

class NoteViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var titleTextField: UITextField!
    @IBOutlet var contentTextView: UITextView!

    // MARK: -

    var note: Note?

    // MARK: - View Life Cycle

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // Update User Interface
        titleTextField.text = note?.title
        contentTextView.text = note?.content
    }

    // MARK: - Actions

    @IBAction func save(_ sender: Any) {
        guard let title = titleTextField.text else { return }
        guard let content = contentTextView.text else { return }

        // Update Note
        note?.title = title
        note?.content = content
        note?.updatedAt = NSDate()

        // Pop View Controller From Navigation Stack
        _ = navigationController?.popViewController(animated: true)
    }

}

In viewWillAppear(_:), we populate the text field and text view with the contents of the note record. In save(_:), the note record is updated with the contents of the text field and text view. Note that we also update the updatedAt property of the note record. The reason for doing so becomes clear later.

Open Main.storyboard and create the user interface of the NoteViewController. Create a Show segue from the prototype cell in the view controller to the note view controller, setting the segue's identifier to SegueNoteViewController.

Exploring the Fetched Results Controller Delegate Protocol | Create Add Note View Controller

Before we can run the application, we need to update prepare(for:sender:) in the ViewController class.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let identifier = segue.identifier else { return }

    switch identifier {
    case "SegueAddNoteViewController":
        guard let navigationController = segue.destination as? UINavigationController else { return }
        guard let viewController = navigationController.viewControllers.first as? AddNoteViewController else { return }

        // Configure View Controller
        viewController.delegate = self
    case "SegueNoteViewController":
        guard let indexPath = tableView.indexPathForSelectedRow else { return }
        guard let viewController = segue.destination as? NoteViewController else { return }

        // Fetch Note
        let note = fetchedResultsController.object(at: indexPath)

        // Configure View Controller
        viewController.note = note
    default:
        print("Unknown Segue")
    }
}

To make sure the row is only selected when the user taps the row, we need to implement tableView(_:didSelectRowAt:) of the UITableViewDelegate protocol.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
}

Run the application, tap a note, make some changes, and tap the Save button. The table view of the ViewController class should automatically be updated.

To make the example a bit more interesting, I suggest we sort the notes by the updatedAt property. The result is that the last modified note is moved to the top of the table view. To accomplish this, we update the sort descriptor of the fetchedResultsController property. Don't forget to set the ascending parameter to false.

// Add Sort Descriptors
let sortDescriptor = NSSortDescriptor(key: "updatedAt", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]

Deleting Notes

Because the NSFetchedResultsController class does most of the heavy lifting for us, deleting notes is as easy as implementing tableView(_:commit:forRowAt:) of the UITableViewDataSource protocol. The NSFetchedResultsController class and its delegate protocol take care of the rest.

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
    guard editingStyle == .delete else { return }

    // Fetch Note
    let note = fetchedResultsController.object(at: indexPath)

    // Delete Note
    fetchedResultsController.managedObjectContext.delete(note)
}

For many applications, the NSFetchedResultsController class is a time saver and it helps to keep view controllers focused on what is specific to your application.

Questions? Leave them in the comments below or reach out to me on Twitter. You can download the source files of this tutorial from GitHub.

Now that you know what Core Data is and how the Core Data stack is set up, it's time to write some code. If you're serious about Core Data, check out Mastering Core Data With Swift. We build an application that is powered by Core Data and you learn everything you need to know to use Core Data in your own projects.

Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy