5.
NSFetchedResults Controller
Written by Pietro Rea
If you followed the previous chapters closely, you probably noticed that most of the sample projects use table views. That’s because Core Data fits nicely with table views. Set up your fetch request, fetch an array of managed objects and plug the result into the table view’s data source. This is a common, everyday scenario.
If you see a tight relationship between Core Data and UITableView, you’re in good company. The authors of the Core Data framework at Apple thought the same way! In fact, they saw so much potential for a close connection between UITableView and Core Data they penned a class to formalize this bond: NSFetchedResultsController.
As the name suggests, 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.
Set up an NSFetchedResultsController correctly, and your table will “magically” mimic its data source without you have to write more than a few lines of code. In this chapter, you’ll learn the ins and outs of this class. You’ll also learn when to use it and when not to use it. Are you ready?
Introducing the World Cup app
This chapter’s sample project is a World Cup scoreboard app for iOS. On startup, the one-page application will list all the teams contesting for the World Cup. Tapping on a country’s cell will increase the country’s wins by one. In this simplified version of the World Cup, the country with the most taps wins the tournament. This ranking simplifies the real elimination rules quite a bit, but it’s good enough for demonstration purposes.
Go to this chapter’s files and find the starter folder. Open WorldCup.xcodeproj. Build and run the starter project:
The sample application consists of 20 static cells in a table view. Those bright blue boxes are where the teams’ flags should be. Instead of real names, you see “Team Name.“ Although the sample project isn’t too exciting, it actually does a lot of the setup for you.
Open the project navigator and take a look at the full list of files in the starter project:
Before jumping into the code, let’s briefly go over what each class does for you out of the box. You’ll find a lot of the setup you did manually in previous chapters comes already implemented for you. Hooray!
-
CoreDataStack: As in previous chapters, this object wraps an instance of
NSPersistentContainer, which in turn contains the cadre of Core Data objects known as the “stack”; the context, the model, the persistent store and the persistent store coordinator. No need to set this up. It comes ready-to-use. -
ViewController: The sample project is a one-page application, and this file represents that one page. On first launch, the view controller reads from seed.json, creates corresponding Core Data objects and saves them to the persistent store. If you’re curious about its UI elements, head over to Main.storyboard. There’s a table, a navigation bar and a single prototype cell.
-
Team+CoreDataClass & Team+CoreDataProperties: These files represent a country’s team. It’s an
NSManagedObjectsubclass with properties for each of its four attributes:teamName,qualifyingZone,imageNameandwins. If you’re curious about its entity definition, head over to WorldCup.xcdatamodel. -
Assets.xcassets: The sample project’s asset catalog contains a flag image for every country in seed.json.
The first three chapters of this book covered the Core Data concepts mentioned above. If “managed object subclass” doesn’t ring a bell or if you’re unsure what a Core Data stack is supposed to do, you may want to go back and reread the relevant chapters. NSFetchedResultsController will be here when you return.
Otherwise, if you’re ready to proceed, you’ll begin implementing the World Cup application. You probably already know who won the World Cup last time, but this is your chance to rewrite history for the country of your choice, with just a few taps!
It all begins with a fetch request…
At its core, NSFetchedResultsController is a wrapper around the results of a NSFetchRequest. Right now, the sample project contains static information. You’re going to create a fetched results controller to display the list of teams from Core Data in the table view.
Open ViewController.swift and add a lazy property to hold your fetched results controller below coreDataStack:
lazy var fetchedResultsController:
NSFetchedResultsController<Team> = {
// 1
let fetchRequest: NSFetchRequest<Team> = Team.fetchRequest()
// 2
let fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.managedContext,
sectionNameKeyPath: nil,
cacheName: nil)
return fetchedResultsController
}()
Like NSFetchRequest, NSFetchedResultsController requires a generic type parameter, Team in this case, to specify the type of entity you expect to be working with.
Let’s go step-by-step through the process:
-
The fetched results controller handles the coordination between Core Data and your table view, but it still needs you to provide an
NSFetchRequest. Remember theNSFetchRequestclass is highly customizable. It can take sort descriptors, predicates, etc.In this example, you get your
NSFetchRequestdirectly from theTeamclass because you want to fetch allTeamobjects. -
The initializer method for a fetched results controller takes four parameters: first up, the fetch request you just created.
The second parameter is an instance of
NSManagedObjectContext. LikeNSFetchRequest, the fetched results controller class needs a managed object context to execute the fetch. It can’t actually fetch anything by itself.The other two parameters are optional:
sectionNameKeyPathandcacheName. Leave them blank for now; you’ll read more about them later in the chapter.
Next, add the following code to the end of viewDidLoad() to actually do the fetching:
do {
try fetchedResultsController.performFetch()
} catch let error as NSError {
print("Fetching error: \(error), \(error.userInfo)")
}
Here you execute the fetch request. If there’s an error, you log the error to the console.
But wait a minute… where are your fetched results? While fetching with NSFetchRequest returns an array of results, fetching with NSFetchedResultsController doesn’t return anything.
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 {
fetchedResultsController.sections?.count ?? 0
}
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int)
-> Int {
guard let sectionInfo =
fetchedResultsController.sections?[section] else {
return 0
}
return sectionInfo.numberOfObjects
}
The number of sections in the table view corresponds to the number of sections in the fetched results controller. You may be wondering how this table view can have more than one section. Aren’t you simply fetching and displaying all teams?
That’s correct. You will only have one section this time around, but keep in mind that NSFetchedResultsController can split up your data into sections. You’ll see an example of this later in the chapter.
Furthermore, the number of rows in each table view section corresponds to the number of objects in each fetched results controller section. You can query information about a fetched results controller section through its sections property.
Note: The
sectionsarray contains opaque objects that implement theNSFetchedResultsSectionInfoprotocol. This lightweight protocol provides information about a section, such as its title and number of objects.
Implementing tableView(_:cellForRowAt:) would typically be the next step.
A quick look at the method, however, reveals it’s already vending TeamCell 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:
func configure(cell: UITableViewCell,
for indexPath: IndexPath) {
guard let cell = cell as? TeamCell else {
return
}
let team = fetchedResultsController.object(at: indexPath)
cell.teamLabel.text = team.teamName
cell.scoreLabel.text = "Wins: \(team.wins)"
if let imageName = team.imageName {
cell.flagImageView.image = UIImage(named: imageName)
} else {
cell.flagImageView.image = nil
}
}
This method takes in a table view cell and an index path. You use this index path to grab the corresponding Team object from the fetched results controller.
Next, you use the Team object to populate the cell’s flag image, team name and score label.
Notice again there’s no array variable holding your teams. They’re all stored inside the fetched results controller and you access them via object(at:).
It’s time to test your creation. Build and run the app. Ready, set and… crash?
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'An instance of NSFetchedResultsController requires a fetch request with sort descriptors'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff2043a126 __exceptionPreprocess + 242
1 libobjc.A.dylib 0x00007fff20177f78 objc_exception_throw + 48
2 CoreData 0x00007fff25293b57 -[NSFetchedResultsController _keyPathContainsNonPersistedProperties:] + 0
--- snip! ---
30 WorldCup 0x0000000108e086db main + 75
)
libc++abi.dylib: terminating with uncaught exception of type NSException
What happened? NSFetchedResultsController is helping you out here, though it may not feel like it!
If you want to use it to populate a table view and have it know which managed object should appear at which index path, you can’t just throw it a basic fetch request.
The key part of the crash log is this:
'An instance of NSFetchedResultsController requires a fetch request with sort descriptors'
A regular fetch request doesn’t require a sort descriptor.
Its minimum requirement is you set an entity description, and it will fetch all objects of that entity type. NSFetchedResultsController, however, requires at least one sort descriptor. Otherwise, how would it know the right order for your table view?
Go back to the fetchedResultsController lazy property and add the following lines after let fetchRequest: NSFetchRequest<Team> = Team.fetchRequest():
let sort = NSSortDescriptor(
key: #keyPath(Team.teamName),
ascending: true)
fetchRequest.sortDescriptors = [sort]
Adding this sort descriptor will show the teams in alphabetical order from A to Z and fix the earlier crash. Build and run the application.
Success! The full list of World Cup participants is on your device or iOS Simulator. Notice, however, that every country has zero wins and there’s no way to increment the score. Some people say soccer is a low-scoring sport, but this is absurd!
Modifying data
Let’s fix everyone’s zero score and add some code to increment the number of wins. Still in ViewController.swift, replace the currently empty implementation of the table view delegate method tableView(_:didSelectRowAt:) with the following:
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
let team = fetchedResultsController.object(at: indexPath)
team.wins += 1
coreDataStack.saveContext()
}
When the user taps a row, you grab the Team corresponding to the selected index path, increment its number of wins and commit the change to Core Data’s persistent store.
You might think a fetched results controller is only good for fetching results from Core Data, but the Team objects you get back are the same old managed object subclasses. You can update their values and save just as you’ve always done.
Build and run once again, and tap on the first country on the list (Algeria) three times:
What’s going on here? You’re tapping away, but the number of wins isn’t going up. You’re updating Algeria’s number of wins in Core Data’s underlying persistent store, but you aren’t triggering a UI refresh.
Go back to Xcode, stop the app, and build and run again.
Just as you suspected, re-launching the app from scratch forced a UI refresh, showing Algeria’s real score of 3. NSFetchedResultsController has a nice solution to this problem, but for now, let’s use the brute force solution.
Add the follow line to the end of tableView(_:didSelectRowAt:):
tableView.reloadData()
In addition to incrementing a team’s number of wins, tapping a cell now reloads the entire table view. This approach is heavy-handed, but it does the job for now. Build and run the app one more time.
Tap as many countries as you want, as many times as you want. Verify that the UI is always up to date.
There you go. You’ve got a fetched results controller up and running. Excited?
If this were all NSFetchedResultsController could do, you would probably feel a little disappointed. After all, you can accomplish the same thing using an NSFetchRequest and a simple array.
The real magic comes in the remaining sections of this chapter. NSFetchedResultsController earns its keep in the Cocoa Touch frameworks with features such as section handling and change monitoring, which you’ll cover next.
Grouping results into sections
There are six qualifying zones in the World Cup: Africa, Asia, Oceania, Europe, South America and North/Central America. The Team entity has a string attribute named qualifyingZone storing this information.
In this section, you’ll split up the list of countries into their respective qualifying zones. 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: coreDataStack.managedContext,
sectionNameKeyPath: #keyPath(Team.qualifyingZone),
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. In this case, it will generate sections for each unique value of qualifyingZone such as “Africa”, “Asia”, “Oceania“ and so on. This is exactly what you want!
Note:
sectionNameKeyPathtakes a keyPath string. It can take the form of an attribute name such asqualifyingZoneorteamName, or it can drill deep into a Core Data relationship, such asemployee.address.street. Use the#keyPathsyntax to defend against typos and stringly typed code.
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
}
Implementing this data source method adds section headers to the table view, making it easy to see where one section ends and another one begins. In this case, the section gets its title from the qualifying zone. Like before, this information comes directly from the NSFetchedResultsSectionInfo protocol.
Build and run the application. Your app will look something like the following:
Scroll down the page. There’s good news and bad news. The good news is the app accounts for all six sections. Hooray! The bad news is the world is upside down.
Take a closer look at the sections. You’ll see Argentina in Africa, Cameroon in Asia and Russia in South America. How did this happen? It’s not a problem with the data; you can open seed.json and verify each team lists the correct qualifying zone.
Have you figured it out? The list of countries is still shown alphabetically and the fetched results controller is simply splitting up the table into sections as if all teams of the same qualifying zone were grouped together.
Go back to your lazily-instantiated NSFetchedResultsController property and make the following change to fix the problem.
Replace the existing code that creates and sets the sort descriptor on the fetch request with the following:
let zoneSort = NSSortDescriptor(
key: #keyPath(Team.qualifyingZone),
ascending: true)
let scoreSort = NSSortDescriptor(
key: #keyPath(Team.wins),
ascending: false)
let nameSort = NSSortDescriptor(
key: #keyPath(Team.teamName),
ascending: true)
fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort]
The problem was the sort descriptor. This is another NSFetchedResultsController “gotcha” to keep in mind. If you want to separate fetched results using a section keyPath, the first sort descriptor’s attribute must match the key path’s attribute.
The documentation for NSFetchedResultsController makes this point emphatically, and with good reason! You saw what happened when the sort descriptor doesn’t match the key path — there’s no sense to your data.
Build and run one more time to verify this change fixed the problem:
Indeed it did. Changing the sort descriptor restored the geopolitical balance in your sample application. African teams are in Africa, European teams are in Europe and so on.
Note: The only team that may still raise eyebrows is Australia, which appears under Asia’s qualifying zone. This is how FIFA categorizes Australia. If you don’t like it, you can file a bug report with them!
Notice that within each qualifying zone, teams are sorted by number of wins from highest to lowest, then by name. This is because in the previous code snippet, you added three sort descriptors: first sort by qualifying zone, then by number of wins, then finally by name.
Before moving on, take a moment to think of what you would have needed to do to separate the teams by qualifying zone without the fetched results controller. First, you would have had to create a dictionary and iterate over the teams to find unique qualifying zones.
As you traversed the array of teams, you would have had to associate each team with the correct qualifying zone. Once you had the list of teams by zone, you’d then would have had to sort the data.
Of course it’s not impossible to do this yourself, but it’s tedious. This is what NSFetchedResultsController saved you from doing. You can take the rest of the day off and go to the beach or watch some old World Cup matches. Thank you, NSFetchedResultsController!
“Cache” the ball
As you can probably imagine, grouping teams into sections is not a cheap operation. There’s no way to avoid iterating over every team.
It’s not a performance problem in this case, because there are only 32 teams to consider. But imagine what would happen if your data set were much larger. What if your task were to iterate over 3 million census records and separate them by state or province?
“I’d just throw that on a background thread!” might be your first thought. The table view, however, can’t populate itself until all sections are available. You might save yourself from blocking the main thread, but you’d still be left looking at a spinner. There’s no denying that this operation is expensive. At a bare minimum, you should only pay the cost once: figure out the section grouping a single time, and reuse your result every time after that.
The authors of NSFetchedResultsController thought about this problem and came up with a solution: caching. You don’t have to do much to turn it on.
Head back to your lazily instantiated NSFetchedResultsController and make the following modification to the fetched results controller initialization, adding a value to the cacheName parameter:
let fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.managedContext,
sectionNameKeyPath: #keyPath(Team.qualifyingZone),
cacheName: "worldCup")
You specify a cache name to turn on NSFetchedResultsController’s on-disk section cache. That’s all you need to do! Keep in mind that this section cache is completely separate from Core Data’s persistent store, where you persist the teams.
Note:
NSFetchedResultsController’s section cache is very sensitive to changes in its fetch request. As you can imagine, any changes — such as a different entity description or different sort descriptors — would give you a completely different set of fetched objects, invalidating the cache completely. If you make changes like this, you must delete the existing cache usingdeleteCache(withName:)or use a different cache name.
Build and run the application a few times. The second launch should be a little bit faster than the first. This is not the author’s power of suggestion (psst, say “fast” five times in a row); it’s NSFetchedResultsController’s cache system at work!
On the second launch, NSFetchedResultsController reads directly from your cache. This saves a round trip to Core Data’s persistent store, as well as the time needed to compute those sections. Hooray!
You’ll learn about measuring performance and seeing if your code changes really did make things faster in Chapter 8, “Measuring & Boosting Performance”.
In your own apps, consider using NSFetchedResultsController’s cache if you’re grouping results into sections and either have a very large data set or are targeting older devices.
Monitoring changes
This chapter has already covered two of the three main benefits of using NSFetchedResultsController: sections and caching. The third and last benefit is somewhat of a double-edged sword: it’s powerful but also easy to misuse.
Earlier in the chapter, when you implemented the tap to increment the number of wins, you added a line of code to reload the table view to show the updated score. 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.
Not to get too philosophical, but the root problem is change. Something changed in the underlying data and you had to be explicit about reloading the user interface.
Imagine what a second version of the World Cup app would look like. Maybe there’s a detail screen for every team where you can change the score.
Maybe the app calls an API endpoint and gets new score information from the web service. It would be your job to refresh the table view for every code path that updates the underlying data.
Doing it explicitly is error-prone, not to mention a little boring. Isn’t there a better way? Yes, there is. Once again, fetched results controller comes to the rescue.
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.
What does it mean a fetched results controller can monitor changes in its “result set”? It means it can monitor changes in all objects, old and new, it would have fetched, in addition to objects it has already fetched. This distinction will become clearer later in this section.
Let’s see this in practice. Still in ViewController.swift, add the following extension to the bottom of the file:
// MARK: - NSFetchedResultsControllerDelegate
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
That’s all you need to start monitoring changes! Of course, the next step is to do something when those change reports come in. You’ll do that next.
Note: A fetched results controller can only monitor changes made via the managed object context specified in its initializer. If you create a separate
NSManagedObjectContextsomewhere else in your app and start making changes there, your delegate method won’t run until those changes have been saved and merged with the fetched results controller’s context.
Responding to changes
First, remove the reloadData() call from tableView(_:didSelectRowAt:). As mentioned before, this was the brute force approach that you’re now going to replace.
NSFetchedResultsControllerDelegate has four methods that come in varying degrees of granularity. To start out, implement the broadest delegate method, the one that says: “Hey, something just changed!”
Add the following method inside the NSFetchedResultsControllerDelegate extension:
func controllerDidChangeContent(_ controller:
NSFetchedResultsController<NSFetchRequestResult>) {
tableView.reloadData()
}
The change may seem small, but implementing this method means that any change whatsoever, no matter the source, will refresh the table view. Build and run the application. Verify that the table view’s cells still update correctly by tapping on a few cells:
The score labels update as before, but there’s something else happening. When one country has more points than another country in the same qualifying zone, that country will “jump” up a level. This is the fetched results controller noticing a change in the sort order of its fetched results and readjusting the table view’s data source accordingly.
When the cells do move around, it’s pretty jumpy… almost as if you were completely reloading the table every time something changed.
Next, you’ll go from reloading the entire table to refreshing only what needs to change. The fetched results controller delegate can tell you if a specific index path needs to be moved, inserted or deleted due to a change in the fetched results controller’s result set.
Replace the contents of the NSFetchedResultsControllerDelegate extension, with the following three delegate methods to see this in action:
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! TeamCell
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()
}
Whew! That’s a wall of code. Fortunately, it’s mostly boilerplate and easy to understand. Let’s briefly go over all three methods you just added or modified.
-
controllerWillChangeContent(_:): This delegate method notifies you that changes are about to occur. You ready your table view using
beginUpdates(). -
controller(_:didChange:at:for:newIndexPath:): This method is quite a mouthful. And with good reason — it tells you exactly which objects changed, what type of change occurred (insertion, deletion, update or reordering) and what the affected index paths are.
This middle method is the proverbial glue that synchronizes your table view with Core Data. No matter how much the underlying data changes, your table view will stay true to what’s going on in the persistent store.
-
controllerDidChangeContent(_:): The delegate method you had originally implemented to refresh the UI turned out to be the third of three delegate methods that notify you of changes. Rather than refreshing the entire table view, you just need to call
endUpdates()to apply the changes.
Note: What you end up doing with the change notifications depends on your individual app. The implementation you see above is an example Apple provided in the
NSFetchedResultsControllerDelegatedocumentation.
Note the order and nature of the methods ties in very neatly to the “begin updates, make changes, end updates” pattern used to update table views. This is not a coincidence!
Build and run to see your work in action. Right off the bat, each qualifying zone lists teams by the number of wins. Tap on different countries a few times. You’ll see the cells animate smoothly to maintain this order.
First:
Then:
For example, in the first screenshot, Switzerland leads Europe with six wins. Tapping on Bosnia & Herzegovina until their score is also six moves the cell on top of Switzerland with a nice animation. This is the fetched results controller delegate in action!
There is one more NSFetchedResultsControllerDelegate method to explore in this section. 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.
Take a moment and think about what kind of change would trigger these notifications. Maybe if a new team entered the World Cup from a completely new qualifying zone, the fetched results controller would pick up on the uniqueness of this value and notify its delegate about the new section.
This would never happen in a standard-issue World Cup. Once the 32 qualifying teams are in the system, there’s no way to add a new team. Or is there?
Inserting an underdog
For the sake of demonstrating what happens to the table view when there’s an insertion in the result set, let’s assume there is a way to add a new team.
If you were paying close attention, you may have noticed the + bar button item on the top-right. It’s been disabled all this time.
Let’s implement this now. In ViewController.swift add the following method below viewDidLoad():
override func motionEnded(
_ motion: UIEvent.EventSubtype,
with event: UIEvent?) {
if motion == .motionShake {
addButton.isEnabled = true
}
}
You override motionEnded(_:with:) so shaking the device enables the + bar button item. This will be your secret way in. The addButton property held a reference to this bar button item all along!
Next, add the following extension above the extension marked with // MARK: - Internal:
// MARK: - IBActions
extension ViewController {
@IBAction func addTeam(_ sender: Any) {
let alertController = UIAlertController(
title: "Secret Team",
message: "Add a new team",
preferredStyle: .alert)
alertController.addTextField { textField in
textField.placeholder = "Team Name"
}
alertController.addTextField { textField in
textField.placeholder = "Qualifying Zone"
}
let saveAction = UIAlertAction(
title: "Save",
style: .default
) { [unowned self] _ in
guard
let nameTextField = alertController.textFields?.first,
let zoneTextField = alertController.textFields?.last
else {
return
}
let team = Team(
context: self.coreDataStack.managedContext)
team.teamName = nameTextField.text
team.qualifyingZone = zoneTextField.text
team.imageName = "wenderland-flag"
self.coreDataStack.saveContext()
}
alertController.addAction(saveAction)
alertController.addAction(UIAlertAction(title: "Cancel",
style: .cancel))
present(alertController, animated: true)
}
}
This is a fairly long but easy-to-understand method. When the user taps the Add button, it presents an alert controller prompting the user to enter a new team.
The alert view has two text fields: one for entering a team name and another for entering the qualifying zone. Tapping Save commits the change and inserts the new team into Core Data’s persistent store.
The action is already connected in the storyboard, so there’s nothing more for you to do. Build and run the app one more time.
If you’re running on a device, shake it. If you’re running on the Simulator, press Command + Control + Z to simulate a shake event.
Open sesame! After much negotiation, both parties decided to “shake on it” and the Add button is now active!
The World Cup is officially accepting one new team. Scroll down the table to the end of the European qualifying zone and the beginning of the North, Central America & Caribbean qualifying zone. You’ll see why in a moment.
Before moving on, take a few seconds to take this in. You’re going to change history by adding another team to the World Cup. Are you ready?
Tap the + button on the top right. You’ll be greeted by an alert view asking for the new team’s details.
Enter the fictitious (yet thriving) nation of Wenderland as the new team. Type Internets for qualifying zone and tap Save. After a quick animation, your user interface should look like the following:
Since “Internets” is a new value for the fetched results controller’s sectionNameKeyPath, this operation created both a new section and added a new team to the fetched results controller result set.
That handles the data side of things. Additionally, since you implemented the fetched results controller delegate methods appropriately, the table view responded by inserting a new section with one new row.
That’s the beauty of NSFetchedResultsControllerDelegate. You can set it once and forget it. The underlying data source and your table view will always be synchronized.
As for how the Wenderland flag made it into the app: Hey, we’re developers! We need to plan for all kinds of possibilities.
Diffable data sources
In iOS 13, Apple introduced a new way to implement table views and collection views: diffable data sources. Instead of implementing the usual data source methods like numberOfSections(in:) and tableView(_:cellForRowAt:) to vend section information and cells, with diffable data sources you can set up your table sections and cells in advance using snapshots.
Along with diffable data sources, there is also a new way of using NSFetchedResultsController to monitor changes in a fetch request’s result set.
Let’s start by removing the existing data source implementation from the sample project. Go ahead and delete the entire ViewController extension that conforms to UITableViewDataSource. The comment // MARK: - UITableViewDataSource marks the beginning.
Then, scroll to the top of ViewController and add the following property:
var dataSource: UITableViewDiffableDataSource<String, NSManagedObjectID>?
UITableViewDiffableDataSource is generic for two types — String to represent section identifiers and NSManagedObjectID to represent the managed object identifiers of the different teams.
Next, add the following new method below configure(cell:for):
func setupDataSource()
-> UITableViewDiffableDataSource<String, NSManagedObjectID> {
UITableViewDiffableDataSource(
tableView: tableView
) { [unowned self] (tableView, indexPath, managedObjectID)
-> UITableViewCell? in
let cell = tableView.dequeueReusableCell(
withIdentifier: self.teamCellIdentifier,
for: indexPath)
if let team =
try? coreDataStack.managedContext.existingObject(
with: managedObjectID) as? Team {
self.configure(cell: cell, for: team)
}
return cell
}
}
This method creates your diffable data source. When creating a data source like this, it automatically adds itself as the table view’s data source. Notice that you pass in a closure for configuring cells, instead of having a separate method.
Since the data source is generic for NSManagedObjectID you use existingObject(with:) to turn identifiers into corresponding Team objects to configure each cell.
Since you resolve the Team objects in the datasource closure you need to reimplement configure(cell:for). Replace its implementation with the following:
func configure(cell: UITableViewCell,
for team: Team) {
guard let cell = cell as? TeamCell else {
return
}
cell.teamLabel.text = team.teamName
cell.scoreLabel.text = "Wins: \(team.wins)"
if let imageName = team.imageName {
cell.flagImageView.image = UIImage(named: imageName)
} else {
cell.flagImageView.image = nil
}
}
Next, add the following to in viewDidLoad() after importJSONSeedDataIfNeeded()
dataSource = setupDataSource()
In the previous setup, the table view’s data source was the view controller. The table view data source is now the diffable data source object that you set up earlier.
Now find the NSFetchedResultsControllerDelegate implementation and delete all four delegate methods that you set up in the previous section:
controllerWillChangeContent(_:)controller(_:didChangeContentWith:)controllerDidChangeContent(_:)controller(didChange:atSectionIndex:for:)
In their place, implement the following delegate method:
func controller(
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChangeContentWith
snapshot: NSDiffableDataSourceSnapshotReference) {
let snapshot = snapshot
as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
dataSource?.apply(snapshot)
}
The old delegate methods you deleted told you when the changes were about to happen, what the changes were, and when the changes completed.
These delegate calls lined up nicely with methods in UITableView such as beginUpdates() and endUpdates(), which you no longer need to call because you made the switch to diffable data sources.
Instead, the new delegate method gives you a summary of any changes to the fetched result set and passes you a pre-computed snapshot that you can apply directly to your table view. So much simpler!
Build and run to see where you are at with the new diffable snapshots:
Great! It seems like most things worked, but there are two problems. First, the console is warning you that the table view is laying out its cells before it’s on screen, and the second is that the teams seem to be grouped by qualifying zone but the section headers are gone.
The console warning is happening because things are happening in a different order now. When the view controller was the data source of the table, and you were implementing the old fetched results controller delegate methods, then the table wasn’t asking for any information until it was loaded and added to the screen. Now you’re using a diffable data source, and the first change happens when you call performFetch() on the results controller, which in turn calls controller(_: didChangeContentWith:), which “adds” in all of the rows from the first fetch. You call performFetch() in viewDidLoad(), which happens before the view is added to the window. Phew!
To fix this, you need to perform the first fetch later on. Remove the do / catch statement from viewDidLoad(), since that’s now happening too early in the lifecycle. Implement viewDidAppear(_:), which is called after the view is added to the window:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.performWithoutAnimation {
do {
try fetchedResultsController.performFetch()
} catch let error as NSError {
print("Fetching error: \(error), \(error.userInfo)")
}
}
}
Build and run, and the console warning is gone. Now to fix the section headers.
Why have they disappeared? Earlier, when you removed the implementation of UITableViewDataSource, you also removed tableView(_:titleForHeaderInSection:). This method provided the strings to populate the section headers, and without those strings the headers disappeared.
There is no way to turn these headers back on with UITableViewDiffableDataSource so you’ll take an alternate route. Find the section that implements UITableViewDelegate methods and implement these two:
func tableView(_ tableView: UITableView,
viewForHeaderInSection section: Int) -> UIView? {
let sectionInfo = fetchedResultsController.sections?[section]
let titleLabel = UILabel()
titleLabel.backgroundColor = .white
titleLabel.text = sectionInfo?.name
return titleLabel
}
func tableView(_ tableView: UITableView,
heightForHeaderInSection section: Int)
-> CGFloat {
20
}
Instead of just returning the title the populate the section headers, these two delegate methods create and return the UILabel to display along with the height of the section header.
Build and run to see if that brought back the missing headers:
The section headers are back, but if you tap on any team cell you’ll notice that the number of wins does not go up anymore. The diffable data source is only thinking about which object IDs are in which order in which section. Even if your team moves up in the section because its wins have increased, the data source will just move the existing cell rather than reconfiguring it. You’ll only see the new score when the cell moves off screen and back on again.
To fix this, find tableView(_:didSelectRowAt:) in the UITableViewDelegate section and add the following code before the call to saveContext():
if var snapshot = dataSource?.snapshot() {
snapshot.reloadItems([team.objectID])
dataSource?.apply(snapshot, animatingDifferences: false)
}
Here, you get the existing snapshot, tell it that your team needs reloading, then apply the updated snapshot back to the data source. The data source will then reload the cell for your team. When you save the context, that will trigger the fetched results controller’s delegate method, which will apply any reordering that needs to happen. Build and run again and confirm that everything works as advertised.
If you got this far, pat yourself on the back. Not only did you re-implement the sample project with diffable data sources, but you also modernized how you monitor changes with the new fetched results controller delegate method. Along the way, you also removed a lot of boilerplate that was previously required.
Note: If you are monitoring changes to manage the state of views that don’t support diffable data sources, you should keep in mind there is another
NSFetchedResultsControllerDelegatemethod that gives you a summary of all changes to the fetched results in one shot, but usesCollectionDifference<NSManagedObjectID>to return the results.
Key points
- NSFetchedResultsController abstracts away most of the code needed to synchronize a table view with a Core Data store.
- At its core,
NSFetchedResultsControlleris a wrapper around an NSFetchRequest and a container for its fetched results. - A fetched results controller requires setting at least one sort descriptor on its fetch request. If you forget the sort descriptor, your app will crash.
- You can set a fetched result’s controller sectionNameKeyPath to specify an attribute to group the results into table view sections. Each unique value corresponds to a different table view section.
- Grouping a set of fetched results into sections is an expensive operation. Avoid having to compute sections multiple times by specifying a cache name on your fetched results controller.
- A fetched results controller can listen for changes in its result set and notify its delegate,
NSFetchedResultsControllerDelegate, to respond to these changes. -
NSFetchedResultsControllerDelegatemonitors changes in individual Core Data records (whether they were inserted, deleted or modified) as well as changes to entire sections. - Diffable data sources make working with fetched results controllers and table views easier.
Where to go from here?
You’ve seen how powerful and useful NSFetchedResultsController can be, and you’ve learned how well it works together with a table view. Table views are so common in iOS apps and you’ve seen first hand how the fetched results controller can save you a lot of time and code!
With some adaptation to the delegate methods, you can also use a fetched results controller to drive a collection view — the main difference being that collection views don’t bracket their updates with begin and end calls, so it’s necessary to store up the changes and apply them all in a batch at the end.
There are a few things you should bear in mind before using fetched results controllers in other contexts. Be mindful of how you implement the fetched results controller delegate methods. Even the slightest change in the underlying data will fire those change notifications, so avoid performing any expensive operations that you’re not comfortable performing over and over.
It’s not every day that a single class gets an entire chapter in a book; that honor is reserved for the select few. NSFetchedResultsController is one of them. As you’ve seen in this chapter, the reason this class exists is to save you time.
NSFetchedResultsController is important for another reason: it fills a gap that iOS developers have faced compared to their macOS developer counterparts. Unlike iOS, macOS has Cocoa bindings, which provide a way to tightly couple a view with its underlying data model. Sound familiar?
If you ever find yourself writing complex logic to compute sections or breaking a sweat trying to get your table view to play nicely with Core Data, think back to this chapter!