Documentation Archive

Developer

Core Data Programming Guide

On This Page

Connecting the Model to Views

In macOS, Core Data is designed to work with the user interface through Cocoa bindings. However, Cocoa bindings are not a part of the user interface in iOS. In iOS, you use NSFetchedResultsController to connect the model (Core Data) to the views (storyboards).

Creating a Fetched Results Controller

When you use Core Data with a UITableView-based layout, the NSFetchedResultsController for your data is typically initialized by the UITableViewController instance that will utilize that data. This initialization can take place in the viewDidLoad or viewWillAppear: methods, or at another logical point in the life cycle of the view controller.

This example shows the initialization of the NSFetchedResultsController.

Objective-C

  1. @property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
  2. - (void)initializeFetchedResultsController
  3. {
  4. NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
  5. NSSortDescriptor *lastNameSort = [NSSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES];
  6. [request setSortDescriptors:@[lastNameSort]];
  7. NSManagedObjectContext *moc = ; //Retrieve the main queue NSManagedObjectContext
  8. [self setFetchedResultsController:[[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:nil cacheName:nil]];
  9. [[self fetchedResultsController] setDelegate:self];
  10. NSError *error = nil;
  11. if (![[self fetchedResultsController] performFetch:&error]) {
  12. NSLog(@"Failed to initialize FetchedResultsController: %@\n%@", [error localizedDescription], [error userInfo]);
  13. abort();
  14. }
  15. }

Swift

  1. var fetchedResultsController: NSFetchedResultsController!
  2. func initializeFetchedResultsController() {
  3. let request = NSFetchRequest(entityName: "Person")
  4. let departmentSort = NSSortDescriptor(key: "department.name", ascending: true)
  5. let lastNameSort = NSSortDescriptor(key: "lastName", ascending: true)
  6. request.sortDescriptors = [departmentSort, lastNameSort]
  7. let moc = dataController.managedObjectContext
  8. fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: nil, cacheName: nil)
  9. fetchedResultsController.delegate = self
  10. do {
  11. try fetchedResultsController.performFetch()
  12. } catch {
  13. fatalError("Failed to initialize FetchedResultsController: \(error)")
  14. }
  15. }

In the initializeFetchedResultsController method shown above that will live within the controlling UITableViewController instance, you first construct a fetch request (NSFetchRequest), which is at the heart of the NSFetchedResultsController. Note that the fetch request contains a sort descriptor (NSSortDescriptor). NSFetchedResultsController requires at least one sort descriptor to control the order of the data that is being presented.

After the fetch request is initialized, you can initialize the NSFetchedResultsController instance. The fetched results controller requires you to pass it an NSFetchRequest instance and a reference to the managed object context (NSManagedObjectContext) that the fetch is to be run against. The sectionNameKeyPath and the cacheName properties are both optional.

After the fetched results controller is initialized, you assign it a delegate. The delegate notifies the table view controller when any changes have occurred to the underlying data structure. Typically, the table view controller is also the delegate to the fetched results controller so that it can receive callbacks whenever there are changes to the underlying data.

Next, you start the NSFetchedResultsController by a call to performFetch:. This call retrieves the initial data to be displayed and causes the NSFetchedResultsController instance to start monitoring the managed object context for changes.

Integrating the Fetched Results Controller with the Table View Data Source

After you integrate the initialized fetched results controller and have data ready to be displayed in the table view, you integrate the fetched results controller with the table view data source (UITableViewDataSource).

Objective-C

  1. #pragma mark - UITableViewDataSource
  2. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  3. {
  4. id cell = [tableView dequeueReusableCellWithIdentifier:CellReuseIdentifier];
  5. NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
  6. // Configure the cell from the object
  7. return cell;
  8. }
  9. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
  10. {
  11. return [[[self fetchedResultsController] sections] count];
  12. }
  13. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  14. {
  15. id< NSFetchedResultsSectionInfo> sectionInfo = [[self fetchedResultsController] sections][section];
  16. return [sectionInfo numberOfObjects];
  17. }

Swift

  1. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  2. guard let cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier", for: indexPath) else {
  3. fatalError("Wrong cell type dequeued")
  4. }
  5. // Set up the cell
  6. guard let object = self.fetchedResultsController?.object(at: indexPath) else {
  7. fatalError("Attempt to configure cell without a managed object")
  8. }
  9. //Populate the cell from the object
  10. return cell
  11. }
  12. override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
  13. return fetchedResultsController.sections!.count
  14. }
  15. override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  16. guard let sections = fetchedResultsController.sections else {
  17. fatalError("No sections in fetchedResultsController")
  18. }
  19. let sectionInfo = sections[section]
  20. return sectionInfo.numberOfObjects
  21. }

As shown in each UITableViewDataSource method above, the integration with the fetched results controller is reduced to a single method call that is specifically designed to integrate with the table view data source.

Communicating Data Changes to the Table View

In addition to making it significantly easier to integrate Core Data with the table view data source, NSFetchedResultsController handles the communication with the UITableViewController instance when data changes. To enable this, implement the NSFetchedResultsControllerDelegate protocol:

Objective-C

  1. #pragma mark - NSFetchedResultsControllerDelegate
  2. - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
  3. {
  4. [[self tableView] beginUpdates];
  5. }
  6. - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
  7. {
  8. switch(type) {
  9. case NSFetchedResultsChangeInsert:
  10. [[self tableView] insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
  11. break;
  12. case NSFetchedResultsChangeDelete:
  13. [[self tableView] deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
  14. break;
  15. case NSFetchedResultsChangeMove:
  16. case NSFetchedResultsChangeUpdate:
  17. break;
  18. }
  19. }
  20. - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
  21. {
  22. switch(type) {
  23. case NSFetchedResultsChangeInsert:
  24. [[self tableView] insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
  25. break;
  26. case NSFetchedResultsChangeDelete:
  27. [[self tableView] deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
  28. break;
  29. case NSFetchedResultsChangeUpdate:
  30. [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
  31. break;
  32. case NSFetchedResultsChangeMove:
  33. [[self tableView] deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
  34. [[self tableView] insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
  35. break;
  36. }
  37. }
  38. - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
  39. {
  40. [[self tableView] endUpdates];
  41. }

Swift

  1. func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
  2. tableView.beginUpdates()
  3. }
  4. func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
  5. switch type {
  6. case .insert:
  7. tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
  8. case .delete:
  9. tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
  10. case .move:
  11. break
  12. case .update:
  13. break
  14. }
  15. }
  16. func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
  17. switch type {
  18. case .insert:
  19. tableView.insertRows(at: [newIndexPath!], with: .fade)
  20. case .delete:
  21. tableView.deleteRows(at: [indexPath!], with: .fade)
  22. case .update:
  23. tableView.reloadRows(at: [indexPath!], with: .fade)
  24. case .move:
  25. tableView.moveRow(at: indexPath!, to: newIndexPath!)
  26. }
  27. }
  28. func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
  29. tableView.endUpdates()
  30. }

Implementing the four protocol methods shown above provides automatic updates to the associated UITableView whenever the underlying data changes.

Adding Sections

So far you have been working with a table view that has only one section, which represents all of the data that needs to be displayed in the table view. If you are working with a large number of Employee objects, it can be advantageous to divide the table view into multiple sections. Grouping the employees by department makes the list of employees more manageable. Without Core Data, a table view with multiple sections would involve an array of arrays, or perhaps an even more complicated data structure. With Core Data, you make a simple change to the construction of the fetched results controller.

Objective-C

  1. - (void)initializeFetchedResultsController
  2. {
  3. NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
  4. NSSortDescriptor *departmentSort = [NSSortDescriptor sortDescriptorWithKey:@"department.name" ascending:YES];
  5. NSSortDescriptor *lastNameSort = [NSSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES];
  6. [request setSortDescriptors:@[departmentSort, lastNameSort]];
  7. NSManagedObjectContext *moc = [[self dataController] managedObjectContext];
  8. [self setFetchedResultsController:[[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:@"department.name" cacheName:nil]];
  9. [[self fetchedResultsController] setDelegate:self];
  10. }

Swift

  1. func initializeFetchedResultsController() {
  2. let request = NSFetchRequest(entityName: "Person")
  3. let departmentSort = NSSortDescriptor(key: "department.name", ascending: true)
  4. let lastNameSort = NSSortDescriptor(key: "lastName", ascending: true)
  5. request.sortDescriptors = [departmentSort, lastNameSort]
  6. let moc = dataController.managedObjectContext
  7. fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: "department.name", cacheName: nil)
  8. fetchedResultsController.delegate = self
  9. do {
  10. try fetchedResultsController.performFetch()
  11. } catch {
  12. fatalError("Failed to initialize FetchedResultsController: \(error)")
  13. }
  14. }

In this example you add one more NSSortDescriptor instance to the NSFetchRequest instance. You set the same key from that new sort descriptor as the sectionNameKeyPath on the initialization of the NSFetchedResultsController. The fetched results controller uses this initial sort controller to break apart the data into multiple sections and therefore requires that the keys match.

This change causes the fetched results controller to break the returning Person instances into multiple sections based on the name of the department that each Person instance is associated with. The only conditions of using this feature are:

  • The sectionNameKeyPath property must also be an NSSortDescriptor instance.

  • The NSSortDescriptor must be the first descriptor in the array passed to the fetch request.

Adding Caching for Performance

In many situations, a table view represents a relatively static type of data. A fetch request is defined at the creation of the table view controller, and it never changes throughout the life of the application. In those situations it can be advantageous to add a cache to the NSFetchedResultsController instance so that when the application is launched again and the data has not changed, the table view initializes instantaneously. A cache is especially useful for displaying unusually large data sets.

Objective-C

  1. [self setFetchedResultsController:[[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:@"department.name" cacheName:@"rootCache"]];

Swift

  1. fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: "department.name", cacheName: "rootCache")

As shown above, the cacheName property is set when the NSFetchedResultsController instance is initialized, and the fetched results controller automatically gains a cache. Subsequent loading of the data will be nearly instantaneous.