Core Data Fetching: context.fetch() vs NSFetchedResultsController
A practical guide to retrieving data in Core Data, comparing static context.fetch() requests against live-updating NSFetchedResultsControllers.
Core Data Fetching: context.fetch() vs NSFetchedResultsController
If you've spent any time working with Core Data in iOS, you know that saving data is only half the battle. The other half is getting that data back out and onto the screen.
When it comes to retrieving data, Apple gives us two primary tools: the standard context.fetch(_:) and the more robust NSFetchedResultsController. But what is the exact difference, and when should you use which?
Let's dive into both approaches with some real-world code examples.
The Standard Approach: context.fetch()
Calling fetch(_:) on an NSManagedObjectContext is the most straightforward way to query your database. It acts as a one-time snapshot: you ask for data, and Core Data hands you an array of results representing the database at that exact moment.
Code Example
Here is how you might use context.fetch() to get a list of all "Action" movies, sorted alphabetically:
import CoreData
func fetchActionMovies(in context: NSManagedObjectContext) -> [Movie] {
let request: NSFetchRequest<Movie> = Movie.fetchRequest()
// 1. Filter for Action movies
request.predicate = NSPredicate(format: "genre == %@", "Action")
// 2. Sort by name
request.sortDescriptors = [NSSortDescriptor(keyPath: \Movie.name, ascending: true)]
do {
// 3. Execute the fetch synchronously
let movies = try context.fetch(request)
return movies
} catch {
print("Failed to fetch movies: \(error.localizedDescription)")
return []
}
}
When to use it?
- One-off background tasks: If you need to check if a specific record exists before downloading it from the network.
- Static screens: If the data you are displaying will not change while the user is looking at it.
- Data migration or cleanup: When you need to iterate through records to modify or delete them.
The Catch: If a new action movie is added to the database five seconds after you run this function, your movies array will not update. You would have to execute the fetch request all over again to see the new data.
The Live Observer: NSFetchedResultsController
If context.fetch is a photograph, NSFetchedResultsController (FRC) is a live video feed.
An FRC is designed specifically to act as the glue between your Core Data store and your User Interface (like a UITableView, UICollectionView, or a SwiftUI List). Not only does it perform the initial fetch, but it also actively monitors the NSManagedObjectContext for any changes (insertions, deletions, or updates) to the objects that match its fetch request.
Code Example
Here is how you might set up an NSFetchedResultsController to power a live-updating UI:
import CoreData
import Foundation
@Observable
class MovieListViewModel: NSObject {
var movies: [Movie] = []
@ObservationIgnored private let context: NSManagedObjectContext
@ObservationIgnored private var fetchedResultsController: NSFetchedResultsController<Movie>!
init(context: NSManagedObjectContext) {
self.context = context
super.init()
setupFRC()
}
private func setupFRC() {
let request: NSFetchRequest<Movie> = Movie.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Movie.name, ascending: true)]
// 1. Initialize the FRC
fetchedResultsController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil, // Use this for grouping data into sections!
cacheName: nil
)
// 2. Set the delegate to listen for changes
fetchedResultsController.delegate = self
// 3. Perform the initial fetch
do {
try fetchedResultsController.performFetch()
self.movies = fetchedResultsController.fetchedObjects ?? []
} catch {
print("Failed to fetch: \(error.localizedDescription)")
}
}
}
// 4. Respond to live changes
extension MovieListViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let updatedMovies = controller.fetchedObjects as? [Movie] else { return }
// Update our array so the UI re-renders automatically
DispatchQueue.main.async {
self.movies = updatedMovies
}
}
}
(Note: If you are building a modern SwiftUI app, you can also use the @FetchRequest property wrapper, which wraps an NSFetchedResultsController under the hood for you!)
When to use it?
- List-based UIs: Any time you are rendering lists of data in the UI (SwiftUI
List,UITableView). - Reactive Interfaces: When you want your UI to automatically react when data is synced from a remote server or edited in another part of the app.
- Sectioned Data: FRCs have built-in support for dividing your fetched data into sections (e.g., grouping movies by
genre), which makes building sectioned tables incredibly easy.
Summary
Choosing between the two usually comes down to whether your data is meant for the background or the foreground.
Use context.fetch() when you just need a quick answer from your database to make logic decisions or process background data.
Reach for NSFetchedResultsController when you are putting that data in front of the user. It will save you from writing endless notification observers and manual UI refresh logic, allowing you to build a UI that is always perfectly in sync with your local database.