Managing Records With Fetched Results Controllers

Managing Records With Fetched Results Controllers

The NSFetchedResultsController class isn't an essential component of a Core Data application, but it makes working with collections of managed objects much easier. This tutorial introduces you to the, almost magical, NSFetchedResultsController class.

What Is It?

An application powered by Core Data needs to make sure the state of the persistent store is reflected by the user interface and vice versa. If a record is deleted from the persistent store, the user interface needs to be updated, informing the user about this event.

The boilerplate code required to update a table view is pretty lengthy. For every table view that manages a list of records, you need to write the same boilerplate code. By using the NSFetchedResultsController class, you only need to write code that is specific to your application. Trivial tasks, such as updating a table view cell when a record is modified, are handled by the fetched results controller.

A fetched results controller manages the results of a fetch request. It notifies its delegate about any changes that affect the results of that fetch request. It even offers the ability to use an in-memory cache to improve performance.

Even though the NSFetchedResultsController class was designed with table views in mind, it also works great with collection views. In this tutorial, we build a basic notes application that keeps track of your notes. We first need to create a project, set up the Core Data stack, and design the data model.

Setting Up the Project

Creating the Project

Fire up Xcode, create a new project based on the Single View Application template, and set Product Name to Notes. Set Language to Swift and Devices to iPhone. Make sure the checkboxes at the bottom are unchecked.

Managing Records With Fetched Results Controllers | Project Setup

Managing Records With Fetched Results Controllers | Project Setup

Creating the Data Model

Note Entity

Select New > File... from Xcode's File menu and choose the Data Model template from the iOS > Core Data section. Name the data model Notes and click Create. Open Notes.xcdatamodeld to populate the data model.

Managing Records With Fetched Results Controllers | Create Data Model

Attributes

We need to add one entity to the data model, Note. The entity has four attributes:

  • title of type String
  • content of type String
  • createdAt of type Date
  • updatedAt of type Date

Managing Records With Fetched Results Controllers | Create Note Entity

If you want to make sure the data model is set up correctly, download the project from GitHub.

Adding Core Data Manager

Download the project from the previous tutorial and add CoreDataManager.swift to your project.

Adding Notes

Creating the User Interface

Before we can start working with the NSFetchedResultsController class, we need some data to work with. Create a new UIViewController subclass and name it AddNoteViewContorller.

Managing Records With Fetched Results Controllers | Add UIViewController Subclass

Open AddNoteViewController.swift and declare an outlet and two actions. The outlet, titleTextField, is of type UITextField. The bodies of the actions can remain empty for now.

import UIKit

class AddNoteViewController: UIViewController {

    @IBOutlet var titleTextField: UITextField!

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // MARK: - Actions

    @IBAction func save(_ sender: Any) {

    }

    @IBAction func cancel(_ sender: Any) {

    }

}

Open Main.storyboard, select the view controller that is already present, and choose Embed In > Navigation Controller from the Editor menu at the top. Add a bar button item to the navigation bar of the view controller and set System Item to Add in the Attributes Inspector.

Managing Records With Fetched Results Controllers | Embed View Controller in Navigation Controller

Open the Object Library and add a view controller. Set its class to AddNoteViewController in the Identity Inspector and embed the view controller in a navigation controller. Add a bar button item on each side of the navigation bar, setting System Item to Cancel and Save respectively. Connect the cancel(_:) action to the Cancel button and the save(_:) action to the Save button. Add a text field to the view controller, pin it to the top with constraints, and wire it up to the view controller's titleTextField outlet.

Managing Records With Fetched Results Controllers | Create User Interface Add Note View Controller

Press Control and drag from the Add button of the ViewController instance to the navigation controller of the AddViewController instance to create a segue, choosing Action Segue > Present Modally from the menu that pops up. Select the segue and set Identifier to SegueAddNoteViewController in the Attributes Inspector.

Managing Records With Fetched Results Controllers | Create Segue

Implementing Actions

To add the ability to create notes, we need to implement the cancel(_:) and save(_:) actions of the AddNoteViewController class. The cancel(_:) action is easy.

@IBAction func cancel(_ sender: Any) {
    dismiss(animated: true, completion: nil)
}

The save(_:) action requires a bit of preparation. The add note view controller isn't responsible for creating a note. It delegates this task to a delegate. At the top of AddNoteViewController.swift, declare the AddNoteViewControllerDelegate protocol. This protocol declares one method, controller(_:didAddNoteWithTitle:).

protocol AddNoteViewControllerDelegate {
    func controller(_ controller: AddNoteViewController, didAddNoteWithTitle title: String)
}

We also need to declare a delegate property of type AddNoteViewControllerDelegate?.

import UIKit

protocol AddNoteViewControllerDelegate {
    func controller(_ controller: AddNoteViewController, didAddNoteWithTitle title: String)
}

class AddNoteViewController: UIViewController {

    @IBOutlet var titleTextField: UITextField!

    var delegate: AddNoteViewControllerDelegate?

    ...

}

To set the delegate property of the add note view controller, we implement prepare(for:sender:) in ViewController.swift. Because the add note view controller is embedded in a navigation controller, we need to jump through a few hoops to get a hold of the add note view controller.

// MARK: - Navigation

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard segue.identifier == "SegueAddNoteViewController" else { return }
    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
}

We're almost there. In the next step, we conform the ViewController class to the AddNoteViewControllerDelegate protocol. In ViewController.swift, we create an extension for the ViewController class to make it conform to the AddNoteViewControllerDelegate protocol.

extension ViewController: AddNoteViewControllerDelegate {

    func controller(_ controller: AddNoteViewController, didAddNoteWithTitle title: String) {

    }

}

Before we can add a note record, we need a managed object context to add it to. This means we need an instance of the CoreDataManager class. Add a property of type CoreDataManager and initialize an instance. Note that I also added an import statement for the Core Data framework.

import UIKit
import CoreData

class ViewController: UIViewController {

    // MARK: - Properties

    fileprivate let coreDataManager = CoreDataManager(modelName: "Notes")

    ...

}

Adding a Note

Phew. We can finally add a note. We do this in the controller(_:didAddNoteWithTitle:) delegate method. We create a Note instance and populate its properties.

extension ViewController: AddNoteViewControllerDelegate {

    func controller(_ controller: AddNoteViewController, didAddNoteWithTitle title: String) {
        // Create Note
        let note = Note(context: coreDataManager.managedObjectContext)

        // Populate Note
        note.content = ""
        note.title = title
        note.updatedAt = NSDate()
        note.createdAt = NSDate()

        do {
            try note.managedObjectContext?.save()
        } catch {
            let saveError = error as NSError
            print("Unable to Save Note")
            print("\(saveError), \(saveError.localizedDescription)")
        }
    }

}

The final piece of the puzzle is implementing the save(_:) action of the AddNoteViewController class. In this method, we fetch the text of the text field, make sure it isn't an empty string, and notify the delegate.

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

    // Notify Delegate
    delegate.controller(self, didAddNoteWithTitle: title)

    // Dismiss View Controller
    dismiss(animated: true, completion: nil)
}

That's it. Run the application in the simulator or on a physical device to make sure everything is working. You won't see any notes appear in the view controller. We fix that in the next step.

Listing Notes

The responsibility of the ViewController class is to list the notes of the user. This task is perfectly suited for the NSFetchedResultsController class. Before we revisit the storyboard, declare an outlet for the table view we are about to add and conform the ViewController class to the UITableViewDataSource and UITableViewDelegate protocols using an extension.

import UIKit
import CoreData

class ViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView!

    // MARK: -

    fileprivate let coreDataManager = CoreDataManager(modelName: "Notes")

    ...

}
extension ViewController: UITableViewDataSource {

}

extension ViewController: UITableViewDelegate {

}

Open Main.storyboard and add a table view to the View Controller Scene. Connect the table view's dataSource and delegate outlets to the view controller and connect the table view to the view controller's tableView outlet. Add a prototype cell to the table view, set Style to Subtitle, and Identifier to NoteCell.

Managing Records With Fetched Results Controllers | Add Table View to View Controller

As I mentioned earlier, we populate the table view with the help of an NSFetchedResultsController instance. Declare a lazy property, fetchedResultsController, of type NSFetchedResultsController in the ViewController class. Let me explain what's going on.

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)

    return fetchedResultsController
}()

An NSFetchedResultsController instance manages the results of a fetch request and, therefore, we start by creating a fetch request for the Note entity. We sort the note records based on their createdAt property.

The fetched results controller is initialized by invoking init(fetchRequest:managedObjectContext:sectionNameKeyPath:cacheName:). The first argument is the fetch request we created a moment ago. The second argument is an NSManagedObjectContext instance. It's this managed object context that will execute the fetch request. The third and fourth arguments are not important for this discussion. You can ignore them for now.

We can now display the list of notes in the table view by implementing the UITableViewDataSource protocol. We only need to implement the required methods.

An NSFetchedResultsController instance is capable of managing sections and, for that reason, it's a perfect fit for managing the data of a table view. In tableView(_:numberOfRowsInSection:), we ask the fetched results controller for its sections. This returns an array of objects that conform to the NSFetchedResultsSectionInfo protocol. The sectionInfo object stores information about a section, such as the number of objects a particular section contains.

With that in mind, the implementation of tableView(_:numberOfRowsInSection:) becomes easier to understand. We ask the fetched results controller for the section that corresponds to the value of the section parameter and return the value of its numberOfObjects (computed) property.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    guard let sections = fetchedResultsController.sections else {
        return 0
    }

    let sectionInfo = sections[section]
    return sectionInfo.numberOfObjects
}

The second method we need to implement is tableView(_:cellForRowAt:). In this method, we ask the table view for a table view cell, fetch the note record that corresponds to the index path, and populate the table view cell with data stored in the note record.

We fetch the note record that corresponds to the value of indexPath by invoking object(at:) on the fetched results controller. This method is very useful if you use a fetched results controller in combination with a table view. It shows that the NSFetchedResultsController class works especially well with hierarchical data sets.

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

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

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

    return cell
}

Also note that the fetched results controller knows it is managing a collection of note records. There is no need to cast the result of object(at:) to an instance of the Note class.

If you run the application and add a new note, the table view remains empty. Even if you restart the application, no notes are visible in the table view. This is easy to explain, though. We've told the fetched results controller what fetch request we are interested in, but we haven't asked it to perform the fetch request. We can do this in, for example, the view controller's viewDidLoad() method.

override func viewDidLoad() {
    super.viewDidLoad()

    do {
        try fetchedResultsController.performFetch()
    } catch {
        let fetchError = error as NSError
        print("Unable to Save Note")
        print("\(fetchError), \(fetchError.localizedDescription)")
    }
}

The performFetch() method instructs the fetched results controller to execute its fetch request. Under the hood, the fetched request is executed by the managed object context we initialized the fetched results controller with.

If you run the application now, the table view should no longer be empty if you already added one or more notes. There is another problem, though. If you add a new note, the table view isn't automagically updated. That's a problem we solve in the next tutorial.

Questions? Leave them in the comments below or reach out to me on Twitter. You can download the source files of the 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.