Hello! Welcome to the final lesson in our module on Core Data.
In our last session, we focused on making our data fetches scalable and memory-efficient using fetchBatchSize. This is crucial for performance, but it leaves one big question unanswered: How do we efficiently connect this large dataset to a UITableView and keep it perfectly synchronized when data is added, deleted, or changed?
This lesson directly answers that question. Our learning outcome is to use NSFetchedResultsController to efficiently display Core Data objects in a UITableView. This powerful class acts as the ultimate bridge between your data store and your UI, automating much of the complex work involved in keeping them in sync.
Let's dive in and see how it works.
The Problem: Bridging Data and UI Manually
Without a specialized tool, connecting a Core Data backend to a UITableView involves a lot of manual work:
- You fetch an array of objects and store it in a property.
- You implement the
UITableViewDataSourcemethods using this array. - When the data changes (e.g., a user adds or deletes an item), you must:
- Re-fetch the data.
- Update your local array.
- Tell the table view to update itself, often by calling
reloadData(), which is inefficient and loses any animations.
This process is tedious and error-prone. NSFetchedResultsController (FRC) was designed to solve this exact problem.
NSFetchedResultsController: The Automatic Bridge
NSFetchedResultsController is a controller that you configure with an NSFetchRequest. It not only performs the fetch but also registers for notifications from the managed object context. When any data changes that affect the results of its fetch request, the FRC automatically updates its internal state and notifies its delegate, allowing you to update the UITableView with precise, animated changes.

The video below gives a great introduction to the "why" before diving into the implementation.
iOS Dev 29: CoreData NSFetchedResultsController (Note Taking App) | Swift 5, XCode 12
Watch this short clip from a tutorial by Emmanuel Okwara. It clearly explains the problem of manual UI updates and introduces NSFetchedResultsController as the solution.
Watch from 00:56 to 02:00. The key takeaway is that FRC allows your UI to react to database changes rather than you having to imperatively manage those updates.
Step 1: Setting Up the Fetched Results Controller
Setting up an FRC involves three main steps: declaring it, initializing it, and performing the initial fetch. The video tutorial we'll be following provides a complete walkthrough of this process.
iOS Dev 29: CoreData NSFetchedResultsController (Note Taking App) | Swift 5, XCode 12
Let's continue with the same video to see how to set up the NSFetchedResultsController. This section covers creating a helper method for the FRC, initializing it, setting its delegate, and performing the fetch.
Watch from 02:17 to 08:00. Pay close attention to these key steps: Declaration: A fetchedResultsController property is added to the view controller. Initialization: An NSFetchRequest is created, and it's used to initialize the NSFetchedResultsController. Notice the required parameters: the request, the context, and optional sectioning/caching info. Delegate: The view controller is assigned as the FRC's delegate (fetchedResultsController.delegate = self). Fetch: The initial data is loaded by calling try fetchedResultsController.performFetch().
Let's review the code structure from the video.
First, you declare the FRC as a property in your UITableViewController:
import CoreData
class NotesViewController: UITableViewController {
var fetchedResultsController: NSFetchedResultsController<Note>!
// ...
}
Next, you set it up, often in a helper method called from viewDidLoad. A critical requirement is that the NSFetchRequest must have at least one NSSortDescriptor. The FRC needs to know how to order the objects.
func setupFetchedResultsController() {
let request = Note.fetchRequest() // NSFetchRequest<Note>
// FRC requires at least one sort descriptor.
let sort = NSSortDescriptor(key: "lastUpdated", ascending: false)
request.sortDescriptors = [sort]
// Connect back to our previous lesson: batching for performance!
request.fetchBatchSize = 20
// Get the managed object context.
let moc = CoreDataManager.shared.mainContext
// Initialize the FRC.
fetchedResultsController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: moc,
sectionNameKeyPath: nil, // We'll explore this later
cacheName: nil // And this too
)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
// The table view can now be reloaded to show the initial data.
tableView.reloadData()
} catch {
fatalError("Failed to fetch entities: \(error)")
}
}
This setup is also detailed in the official Apple documentation, which is a great reference.
Core Data Programming Guide: Connecting the Model to Views
For a textual reference, you can review Apple's official (though archived) programming guide. It shows the same initialization pattern.
Skim the Swift code example under the "Creating a Fetched Results Controller" section. It reinforces the pattern of creating a request with sort descriptors, initializing the FRC, setting the delegate, and calling performFetch.
Step 2: Adapting the UITableViewDataSource
With the FRC set up, you no longer need a separate array to hold your data. The FRC becomes the single source of truth for the table view. You'll now adapt your UITableViewDataSource methods to query the FRC directly.
iOS Dev 29: CoreData NSFetchedResultsController (Note Taking App) | Swift 5, XCode 12
The next segment of the video shows how to refactor the data source methods to use the FRC instead of a separate data array.
Watch from 08:04 to 11:54. Notice how the logic simplifies: numberOfRowsInSection gets its count from fetchedResultsController.sections![section].numberOfObjects. cellForRowAt gets the specific object using fetchedResultsController.object(at: indexPath).
Here’s what the refactored data source methods look like:
// MARK: - UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
// The FRC knows how many sections there are.
return fetchedResultsController.sections?.count ?? 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// For a given section, the FRC knows how many rows are in it.
let sectionInfo = fetchedResultsController.sections![section]
return sectionInfo.numberOfObjects
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "NoteCell", for: indexPath) as! NoteCell
// Get the object for the current row directly from the FRC.
let note = fetchedResultsController.object(at: indexPath)
// Configure the cell with the object's data.
cell.configure(with: note)
return cell
}
This is much cleaner and less error-prone than managing a separate array.

Step 3: Reacting to Changes with NSFetchedResultsControllerDelegate
This is where the magic happens. By conforming to NSFetchedResultsControllerDelegate, your view controller can receive precise notifications about what changed in the data, allowing for smooth, animated table view updates.
iOS Dev 29: CoreData NSFetchedResultsController (Note Taking App) | Swift 5, XCode 12
The final piece of the puzzle is implementing the delegate methods. This video segment demonstrates how to translate FRC change notifications into table view animations.
Watch from 17:40 to 20:48. Focus on the four key delegate methods and their roles: controllerWillChangeContent: Calls tableView.beginUpdates(). controller(didChange:at:for:newIndexPath:): Uses a switch on the change type to call insertRows, deleteRows, reloadRows, or moveRow. controller(didChangeSection:...): Handles changes to entire sections (not shown in this clip but follows a similar pattern). controllerDidChangeContent: Calls tableView.endUpdates().
Here is a standard implementation of the delegate methods. Wrapping the table view updates between beginUpdates() and endUpdates() allows UITableView to batch the changes and animate them together.
// MARK: - NSFetchedResultsControllerDelegate
extension NotesViewController: NSFetchedResultsControllerDelegate {
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:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .automatic)
}
case .update:
if let indexPath = indexPath {
tableView.reloadRows(at: [indexPath], with: .automatic)
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.moveRow(at: indexPath, to: newIndexPath)
}
@unknown default:
tableView.reloadData()
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
}
With this code in place, whenever you create, update, or delete a Note object anywhere in your app, as long as you save the managed object context, this delegate will be notified, and your table view will update automatically and with animation.
Bonus: Filtering and Sectioning
NSFetchedResultsController also makes advanced features like searching and sectioning straightforward.
-
Filtering: To filter the results, you simply change the
predicateon the FRC'sfetchRequestand callperformFetch()again. The delegate methods will handle updating the UI to show the filtered results. The video resource (LINK) demonstrates this from 14:11 to 16:38. -
Sectioning: To group your table view into sections (e.g., group notes by creation date or author), you provide a
sectionNameKeyPathduring initialization. This must correspond to the first sort descriptor. The FRC handles all the sectioning logic for you.
For more details on these advanced features, the Hacking with Swift article provides excellent examples.
Optimizing Core Data Performance using NSFetchedResultsController
The article we used in our previous lesson also contains a great explanation of sectioning.
Read the short section titled "Adding Sections". It shows how providing a sectionNameKeyPath and an associated NSSortDescriptor is all it takes to automatically section your table view.
A Note for SwiftUI
While this lesson focuses on UITableView and UIKit, the NSFetchedResultsController is still highly relevant in SwiftUI. The main difference is in the delegate implementation. In a SwiftUI/MVVM architecture, you would typically make your ViewModel the NSFetchedResultsControllerDelegate. Then, inside controllerDidChangeContent(_:), you would update an @Published array with the FRC's fetchedObjects. Your SwiftUI List would then automatically re-render based on the changes to that published property.
Conclusion
This lesson completes our module on Core Data! You've gone from setting up a basic stack to implementing efficient, scalable, and reactive data displays.
Key Takeaways:
NSFetchedResultsControlleris the standard and most efficient way to connect a Core Data result set to aUITableView.- It provides high performance through features like fetch batching and faulting, ensuring your app remains responsive even with large datasets.
- Its delegate protocol allows your UI to reactively and automatically update with animations when the underlying data changes.
- Setup requires an
NSFetchRequestwith at least oneNSSortDescriptor. - You use the FRC as the direct data source for your table view, eliminating the need for a separate data array.
Preview of the Next Lesson:
Now that you have a firm grasp on data persistence, we'll shift our focus to another critical area of professional iOS development: UI performance. In the next lesson, we will learn how to profile rendering performance and identify dropped frames using the Core Animation instrument, kicking off our module on UI and Rendering Performance Optimization.