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.
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.
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
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
.
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.
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.
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.
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.
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.