Core Data Fetching: context.fetch() vs NSFetchedResultsController

August 25, 2026

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.