- Magical Record and NSFetchedResultsController: Core Data made easy
- UITableViewController and NSFetchedResultsController
- Magical Record: Simple Core Data
- TableViewController: Detail View
- Magical Records: Edit, Add, and Delete Records
NSFetchedResultsController takes a lot of the work out of managing the display of data returned from a Core Data fetch request. The basic mechanism is to create a fetch request detailing what you are looking for, how to filter it, and how to order the results. This is then attached to a new NSFetchedResultsController, which queries Core Data. The magic of the NSFetchedResultsController is that it can monitor objects stored in Core Data and automatically change the table view to reflect changes to these objects. Once set up you just make changes to your Core Data objects, and leave NSFetchedResultsController to adjust the table accordingly. Even better is that Apple have already written a whole bunch of code to help with the initial setup.
After all that, lets begin to build an app. First, open Xcode and create a new ‘Single-View Application’ project called MagicNotes.
Create a TableViewController That Understands a NSFetchedResultsController
Next, create a new Objective-C class (File>New>Cocoa Touch>Objective-C Class) called CoreDataTableViewController, which is a subclass of UITableViewController. Copy in the following code to the header and implementation files (collated from the Apple Documentation).
// // CoreDataTableViewController.h // // // A copy of code documented on the NSFetchedResultsController and NSFetchedResultsControllerDelegate pages #import <UIKit/UIKit.h> #import <CoreData/CoreData.h> @interface CoreDataTableViewController : UITableViewController <NSFetchedResultsControllerDelegate> @property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController; - (void)performFetch; @end
// // CoreDataTableViewController.m // #import "CoreDataTableViewController.h" @interface CoreDataTableViewController() @property (nonatomic) BOOL beganUpdates; @end @implementation CoreDataTableViewController #pragma mark - Properties @synthesize fetchedResultsController = _fetchedResultsController; @synthesize beganUpdates = _beganUpdates; - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark - Fetching - (void)performFetch { if (self.fetchedResultsController) { NSError *error; [self.fetchedResultsController performFetch:&error]; } [self.tableView reloadData]; } - (void)setFetchedResultsController:(NSFetchedResultsController *)newfrc { NSFetchedResultsController *oldfrc = _fetchedResultsController; if (newfrc != oldfrc) { _fetchedResultsController = newfrc; newfrc.delegate = self; if ((!self.title || [self.title isEqualToString:oldfrc.fetchRequest.entity.name]) && (!self.navigationController || !self.navigationItem.title)) { self.title = newfrc.fetchRequest.entity.name; } if (newfrc) { [self performFetch]; } else { [self.tableView reloadData]; } } } #pragma mark - UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [[self.fetchedResultsController sections] count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return [[[self.fetchedResultsController sections] objectAtIndex:section] name]; } - (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index { return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index]; } - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { return [self.fetchedResultsController sectionIndexTitles]; } #pragma mark - NSFetchedResultsControllerDelegate - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { [self.tableView beginUpdates]; self.beganUpdates = YES; } - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { switch(type) { case NSFetchedResultsChangeInsert: [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeDelete: [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; break; } } - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { switch(type) { case NSFetchedResultsChangeInsert: [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeDelete: [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeUpdate: [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeMove: [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break; } } - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { if (self.beganUpdates) [self.tableView endUpdates]; } @end
For more complicated apps where table rows can be moved you need to consider suspending tracking of changes during certain user operations. For more info check out the Stanford CS193p course material (in fact do it anyway, as it’s a great course). I’m not using (as many do) the CoreDataTableViewController code from that course, as it doesn’t have a commercial license.
So what does all this code do? First it defines some basic functionality: fetching data, reloading the table view, and handling the creation of new NSFetchedResultControllers. Next it defines all the methods necessary to convert results into the specifications for the table view (e.g. number of table sections, number of rows in each section). Finally, it covers the NSFetchedResultsControllerDelegate protocol. On a basic level this monitors for changes in the data, and makes the necessary changes to the table view.
Subclass Your Clever CoreDataTableViewController
The great things about the CoreDataTableViewController is that it’s highly reusable. We just need to subclass it and add a couple of methods so lets do just that. Create a new Objective-C class (File>New>Cocoa Touch>Objective-C Class) called NotesListViewController that is a subclass of CoreDataTableViewController. For now, we just need to add 2 methods.
#pragma mark setup fetched results controller - (void)setupFetchedResultsController { // This is blank for now } #pragma mark tableView methods - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Note Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier]; cell.textLabel.font = [UIFont systemFontOfSize:19.0]; cell.detailTextLabel.font = [UIFont systemFontOfSize:12]; } // We will configure the cell details here return cell; }
and add one line to viewDidLoad
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. [self setupFetchedResultsController]; }
Build the Storyboard
We will now start to build the (very basic) storyboard. Open up one of the storyboards (the choice doesn’t matter – just make sure you test on the same device or make all changes to both storyboards). Delete the view controller that’s already provided, drop a TableViewController onto the storyboard, change its Custom Class to ‘NotesListViewController’, and embed it in a Navigation Controller (Editor>Embed In>Navigation Controller). Click on the table cell, change the style to “Subtitle” and give is the Reuse Identifier “Note Cell”.
That is everything for this part. At this point you can click run and check you have no errors. If not you will see a very boring app with a blank table. In the next part, we will start to populate that table with some dummy data taken from our Core Data storage.