Core Data and Swift: NSFetchedResultsController

In the previous installments of this series, we covered the fundamentals of the Core Data framework. It's time we put our knowledge to use by building a simple application powered by Core Data.

In this tutorial, we'll also meet another star player of the Core Data framework, the NSFetchedResultsController class. The application that we're about to create manages a list of to-do items. With the application, we can add, update, and delete to-do items. You'll quickly learn that the NSFetchedResultsController class makes this very easy to do.

Prerequisites

What I cover in this series on Core Data is applicable to iOS 7+ and OS X 10.10+, but the focus will be on iOS. In this series, I will work with Xcode 7.1 and Swift 2.1. If you prefer Objective-C, then I recommend reading my earlier series on the Core Data framework.

1. Project Setup

Open Xcode, select New > Project... from the File menu, and choose the Single View Application template from the iOS > Application category.

Choosing the Single View Application Template

Name the project Done, set Language to Swift, and set Devices to iPhone. Because I'd like to show you how to create a Core Data application from scratch, make sure that the checkbox labeled Use Core Data is unchecked. Tell Xcode where you want to save the project and hit Create to create the project.

Configuring the Project

2. Core Data Setup

Open AppDelegate.swift and declare three lazy stored properties managedObjectModel of type NSManagedObjectModelmanagedObjectContext of type NSManagedObjectContext, and persistentStoreCoordinator of type NSPersistentStoreCoordinator. If you're confused by this step, then I recommend you revisit the first article of this series, which covers the Core Data stack in detail.

Note that I've also added an import statement for the Core Data framework at the top of AppDelegate.swift.

Remember that we use the lazy keyword to lazily set up the Core Data stack. This means that we instantiate the managed object context, the managed object model, and the persistent store coordinator the moment they are needed by the application. The following implementation should look very familiar.

There are three things you should be aware of. First, the data model, which we'll create next, will be named Done.momd. Second, we'll name the backing store Done and it will be a SQLite database. Third, if the backing store is incompatible with the data model, we invoke abort, killing the application. As I mentioned earlier in this series, while this is fine during development, you should never call abort in production. We'll revisit migrations and incompatibility issues later in this series.

While our application won't crash if you try to run it, the Core Data stack won't get properly set up. The reason is simple, we haven't created a data model yet. Let's take care of that in the next step.

3. Creating the Data Model

Select New > File... from the File menu and choose Data Model from the iOS > Core Data category.

Creating the Data Model

Name the file Done, double-check that it's added to the Done target, and tell Xcode where it needs to be saved.

Naming the Data Model

The data model is going to be very simple. Create a new entity and name it Item. Add three attributes to the entity, name of  type StringcreatedAt of type Date, and done of type Boolean.

Creating the Item Entity

Mark the attributes as required, not optional, and set the default value of the done attribute to NO.

Configuring the Done Attribute

The Core Data stack is set up and the data model is configured correctly. It's time to become familiar with a new class of the Core Data framework, the NSFetchedResultsController class.

4. Managing Data

This article isn't just about the NSFetchedResultsController class, it's about what the NSFetchedResultsController class does behind the scenes. Let me clarify what I mean by this.

If we were to build our application without the NSFetchedResultsController class, we would need to find a way to keep the application's user interface synchronized with the data managed by Core Data. Fortunately, Core Data has an elegant solution to this problem.

Whenever a record is insertedupdated, or deleted in a managed object context, the managed object context posts a notification through notification center. A managed object context posts three types of notifications:

  • NSManagedObjectContextObjectsDidChangeNotification: This notification is posted every time a record in the managed object context is inserted, updated, or deleted.
  • NSManagedObjectContextWillSaveNotification: This notification is posted by the managed object context before pending changes are committed to the backing store.
  • NSManagedObjectContextDidSaveNotification: This notification is posted by the managed object context immediately after pending changes have been committed to the backing store.

The contents of these notifications are identical, that is, the object property of the notification is the NSManagedObjectContext instance that posted the notification and the notification's userInfo dictionary contains the records that were insertedupdated, and deleted.

The gist is that it requires a fair amount of boilerplate code to keep the results of a fetch request up to date. In other words, if we were to create our application without using the NSFetchedResultsController class, we would have to implement a mechanism that monitored the managed object context for changes and update the user interface accordingly. Let's see how the NSFetchedResultsController can help us with this task.

5. Setting Up the User Interface

Working with the NSFetchedResultsController class is pretty easy. An instance of the NSFetchedResultsController class takes a fetch request and has a delegate object. The NSFetchedResultsController object makes sure that it keeps the results of the fetch request up to date by monitoring the managed object context the fetch request was executed by.

If the NSFetchedResultsController object is notified of any changes by the NSManagedObjectContext object of the fetch request, it notifies its delegate. You may be wondering how this is different from a view controller directly monitoring the NSManagedObjectContext object. The beauty of the NSFetchedResultsController class is that it processes the notifications it receives from the NSManagedObjectContext object and tells the delegate only what it needs to know to update the user interface in response to these changes. The methods of the NSFetchedResultsControllerDelegate protocol should clarify this.

The signatures of the above delegate methods reveal the true purpose of the NSFetchedResultsController class. On iOS, the NSFetchedResultsController class was designed to manage the data displayed by a UITableView or a UICollectionView. It tells its delegate exactly which records changed, how to update the user interface, and when to do this.

Don't worry if you're still unsure about the purpose or advantages of the NSFetchedResultsController class. It'll make more sense once we've implemented the NSFetchedResultsControllerDelegate protocol. Let's revisit our application and put the NSFetchedResultsController class to use.

Step 1: Populating the Storyboard

Open the project's main storyboard, Main.storyboard, select the View Controller Scene, and embed it in a navigation controller by selecting Embed In > Navigation Controller from the Editor menu.

Drag a UITableView object in the View Controller Scene, create an outlet in the ViewController class, and connect it in the storyboard. Don't forget to make the ViewController class conform to the UITableViewDataSource and UITableViewDelegate protocols.

Select the table view, open the Connections Inspector, and connect the table view's dataSource and delegate outlets to the View Controller object. With the table view selected, open the Attributes Inspector and set the number of Prototype Cells to 1.

Before we continue, we need to create a UITableViewCell subclass for the prototype cell. Create a new Objective-C subclass, ToDoCell, and set its superclass to UITableViewCell. Create two outlets, nameLabel of type UILabel and doneButton of type UIButton.

Head back to the storyboard, select the prototype cell in the table view, and set the class to ToDoCell in the Identity Inspector. Add a UILabel and a UIButton object to the cell's content view and connect the outlets in the Connections Inspector. With the prototype cell selected, open the Attributes Inspector and set the identifier of the prototype cell to ToDoCell. This identifier will serve as the cell's reuse identifier. The prototype cell's layout should look something like the screenshot below.

Creating the ToDoCell Prototype Cell

Create a new ViewController subclass and name it AddToDoViewController. Open AddToDoViewController.swift, declare an outlet textField of type UITextField and conform the view controller to the UITextFieldDelegate protocol.

Before we add the view controller to the storyboard, add the following two actions to the view controller's implementation file.

Open the storyboard one more time and add a bar button item with an identifier of Add to the right of the navigation bar of the ViewController. Add a new view controller to the storyboard and set its class to AddToDoViewController in the Identity Inspector. With the view controller selected, choose Embed In > Navigation Controller from the Editor menu.

The new view controller should now have a navigation bar. Add two bar button items to the navigation bar, one on the left with an identity of Cancel and one on the right with an identity of Save. Connect the cancel(_:) action to the left bar button item and the save(_:) action to the right bar button item.

Add a UITextField object to the view controller's view and position it 20 points below the navigation bar. The text field should remain at 20 points below the navigation bar. Note that the layout constraint at the top references the top layout guide.

Adding Layout Constraints

Connect the text field with the corresponding outlet in the view controller and set the view controller as the text field's delegate. Finally, control drag from the bar button item of the ViewController to the navigation controller of which the AddToDoViewController is the root view controller. Set the segue type to Present Modally and set the segue's identifier to SegueAddToDoViewController in the Attributes Inspector. That was a lot to take in. The interesting part is yet to come though.

Step 2: Implementing the Table View

Before we can take our application for a spin, we need to implement the UITableViewDataSource protocol in the ViewController class. However, this is where the NSFetchedResultsController class comes into play. To make sure that everything is working return 0 from the tableView(_:numberOfRowsInSection:) method. This will result in an empty table view, but it will allow us to run the application without running into a crash.

To satisfy the compiler, we also need to implement tableView(_:cellForRowAtIndexPath:). At the top of AddToDoViewController.swift, add a constant for the cell reuse identifier.

Implementing tableView(_:cellForRowAtIndexPath:) is pretty simple since we're not doing anything special with the table view cell yet.

Step 3: Save and Cancel

Open the AddToDoViewController class and implement the cancel(_:) and save(_:) methods as shown below. We'll update their implementations later in this tutorial.

Build and run the application in the simulator to see if everything is wired up correctly. You should be able to tap the add button in the top right to bring up the AddToDoViewController and dismiss it by tapping either the cancel or the save button.

6. Adding the NSFetchedResultsController Class

The NSFetchedResultsController class is part of the Core Data framework and it's meant to manage the results of a fetch request. The class was designed to work seamlessly with UITableView and UICollectionView on iOS and NSTableView on OS X. However, it can be used for other purposes as well.

Step 1: Laying the Groundwork

Before we can start working with the NSFetchedResultsController class, however, the ViewController class needs access to an NSManagedObjectContext instance, the NSManagedObjectContext instance we created earlier in the application delegate. Start by declaring a property managedObjectContext of type NSManagedObjectContext! in the header file of the ViewController class. Note that we're also adding an import statement for the Core Data framework at the top.

Open Main.storyboard, select the storyboard's initial view controller, an UINavigationController instance, and set its Storyboard ID to StoryboardIDRootNavigationController in the Identity Inspector.

In the application delegate's application(_:didFinishLaunchingWithOptions:) method, we get a reference to the ViewController instance, the root view controller of the navigation controller, and set its managedObjectContext property. The updated application(_:didFinishLaunchingWithOptions:) method looks as follows:

To make sure that everything is working, add the following print statement to the viewDidLoad() method of the ViewController class.

Step 2: Initializing the NSFetchedResultsController Instance

Open the implementation file of the ViewController class and declare a lazy stored property of type NSFetchedResultsController. Name the property fetchedResultsController. An NSFetchedResultsController instance also has a delegate property that needs to conform to the NSFetchedResultsControllerDelegate protocol. Because the ViewController instance will serve as the delegate of the NSFetchedResultsController instance, we need to conform the ViewController class to the NSFetchedResultsControllerDelegate protocol as shown below.

It's time to initialize the NSFetchedResultsController instance. The heart of a fetched results controller is the NSFetchRequest object, because it determines which records the fetched results controller will manage. In the view controller's viewDidLoad() method, we initialize the fetch request by passing "Item" to the initWithEntityName(_:) method. This should be familiar by now and so is the next line in which we add sort descriptors to the fetch request to sort the results based on the value of the createdAt attribute of each record.

The initialization of the fetched results controller is pretty straightforward. The init(fetchRequest:managedObjectContext:sectionNameKeyPath:cacheName:) method takes four arguments:

  • the fetch request
  • the managed object context the fetched results controller will be monitoring
  • a section key path if you want the results to be split up into sections
  • a cache name if you want to enable caching

We pass nil for the last two parameters for now. The first argument is obvious, but why do we need to pass a NSManagedObjectContext object as well? Not only is the passed in managed object context used to execute the fetch request, it is also the managed object context that the fetched results controller will be monitoring for changes. This will become clearer in a few minutes when we start implementing the delegate methods of the NSFetchedResultsControllerDelegate protocol.

Finally, we need to tell the fetched results controller to execute the fetch request we passed it. We do this by invoking performFetch() in viewDidLoad(). Note that this is a throwing method, which means we need to wrap it in a do-catch statement. The performFetch() method is similar to the executeFetchRequest(_:) method of the NSManagedObjectContext class.

Step 3: Implementing the Delegate Protocol

With the fetched results controller set up and ready to use, we need to implement the NSFetchedResultsControllerDelegate protocol. As we saw earlier, the protocol defines five methods, three of which are of interest to us in this tutorial:

  • controllerWillChangeContent(controller: NSFetchedResultsController)
  • controllerDidChangeContent(controller: NSFetchedResultsController)
  • controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)

The first two methods tell us when the data the fetched results controller is managing will and did change. This is important to batch update the user interface. For example, it is possible that multiple changes occur at the same time. Instead of updating the user interface for every change, we batch update the user interface once all changes have been made.

In our example, this boils down to the following implementations of controllerWillChangeContent(controller: NSFetchedResultsController) and controllerDidChangeContent(controller: NSFetchedResultsController).

The implementation of controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) is a bit trickier. This delegate method takes no less than five arguments:

  • the NSFetchedResultsController instance
  • the NSManagedObject instance that changed
  • the current index path of the record in the fetched results controller
  • the type of change, that is, insertupdate, or delete
  • the new index path of the record in the fetched results controller, after the change

Note that the index paths have nothing to do with our table view. An NSIndexPath is nothing more than an object that contains one or more indexes to represent a path in a hierarchical structure hence the class's name.

Internally the fetched results controller manages a hierarchical data structure and it notifies its delegate when that data structure changes. It's up to us to visualize those changes in, for example, a table view.

The implementation of controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) looks daunting, but let me walk you through it.

The change type is of type NSFetchedResultsChangeType. This enumeration has four member values:

  • Insert
  • Delete
  • Move
  • Update

The names are pretty self-explanatory. If the type is Insert, we tell the table view to insert a row at newIndexPath. Similarly, if the type is Delete, we remove the row at indexPath from the table view.

If a record is updated, we update the corresponding row in the table view by invoking configureCell(_:atIndexPath:), a helper method that accepts a ToDoCell object and an NSIndexPath object. We'll implement this method in a moment.

If the change type is equal to Move, we remove the row at indexPath and insert a row at newIndexPath to reflect the record's updated position in the fetched results controller's internal data structure.

Step 4: Implementing the UITableViewDataSource Protocol

That wasn't too difficult. Was it? Implementing the UITableViewDataSource protocol is much easier, but there are a few things you should be aware of. Let's start with numberOfSectionsInTableView(_:) and tableView(_:numberOfRowsInSection:.

Even though the table view in our sample application will only have one section, let's ask the fetched results controller for the number of sections. We do this by calling sections on it, which returns an array of objects that conform to the NSFetchedResultsSectionInfo protocol.

Objects conforming to the NSFetchedResultsSectionInfo protocol need to implement a few methods and properties, including numberOfObjects. This gives us what we need to implement the first two methods of the UITableViewDataSource protocol.

Next up are tableView(_:cellForRowAtIndexPath:) and configureCell(_:atIndexPath:). The implementation of tableView(_:cellForRowAtIndexPath:) is short, because we move most of the cell's configuration to configureCell(_:atIndexPath:). We ask the table view for a reusable cell with reuse identifier ReuseIdentifierToDoCell and pass the cell and the index path to configureCell(_:atIndexPath:).

The magic happens in configureCell(_:atIndexPath:). We ask the fetched results controller for the item at indexPath. The fetched results controller returns an NSManagedObject instance to us. We update the nameLabel and the state of the doneButton by asking the record for its name and done attributes.

We'll revisit the UITableViewDataSource protocol later in this tutorial when we delete items from the list. We first need to populate the table view with some data.

7. Adding Records

Let's finish this tutorial by adding the ability to create to-do items. Open the AddToDoViewController class, add an import statement for the Core Data framework, and declare a property managedObjectContext of type NSManagedObjectContext!.

Head back to the ViewController class and implement the prepareForSegue(_:sender:) method. In this method, we set the managedObjectContext property of the AddToDoViewController instance. If you've worked with storyboards before, then the implementation of prepareForSegue(_:sender:) should be straightforward.

If the user enters text in the text field of the AddToDoViewController and taps the Save button, we create a new record, populate it with data, and save it. This logic goes into the save(_:) method we created earlier.

The save(_:) method looks pretty impressive, but there's nothing in there that we haven't covered yet. We create a new managed object, using an NSEntityDescription instance and an NSManagedObjectContext instance. We then populate the managed object with a name and date.

If saving the managed object context is successful, we dismiss the view controller, otherwise we show an alert by invoking showAlertWithTitle(_:message:cancelButtonTitle:), a helper method. If the user taps the save button without entering any text, we also show an alert. In showAlertWithTitle(_:message:cancelButtonTitle:), we create, configure, and present a UIAlertController.

Run the application and add a few items. I'm sure you agree that the NSFetchedResultsController class makes the process of adding items incredibly easy. It takes care of monitoring the managed object context for changes and we update the user interface, the table view of the ViewController class, based on what the NSFetchedResultsController instance tells us through the NSFetchedResultsControllerDelegate protocol.

Conclusion

In the next article, we'll finish the application by adding the ability to delete and update to-do items. It's important that you understand the concepts we discussed in this article. The way Core Data broadcasts the changes of a managed object context's state is essential so make sure you understand this before moving on.

Tags:

Comments

Related Articles