Using State Preservation with Core Data
In previous post on state preservation and restoration, I presented the basics. This post deals with something that’s fairly common in data-driven apps.
Core Data is one of my favourite iOS frameworks; I use it in almost all of my apps, anything that requires even a minor object graph. So, let’s see how to save/restore state of table views (it’s the same for collection views) fueled by NSFetchedResultsController
.
In Couch to 5k apps, this Runs tab contains UITableViewController
subclass. We know from previous post that we should adopt UIDataSourceModelAssociation
protocol for its data source:
@interface ProgressViewController : UITableViewController < NSFetchedResultsControllerDelegate, ..., UIDataSourceModelAssociation >
To refresh your memory - this view controller is created in App Delegate, as top level VC for the Runs tab. Thus I only need to set its restorationIdentifier
and UIKit will do the rest (no need for the restoration class).
- (id)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) {
self.restorationIdentifier = RTRunStateIdentifierPROGRESS;
...
}
...
}
Saving state
To save the state, the main thing is to choose the modelIdentifier - a string to uniquely identify NSManagedObject
(subclass) that corresponds to each cell.
The obvious choice is NSManagedObjectID as it’s universal identifier for the given object. To convert it into string, use [[NSManagedObjectID URIRepresentation] absoluteString]
thus for my case the method looks like this:
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view {
if (!idx || !view) return nil;
NSManagedObject *oneSession = [self.fetchedResultsController objectAtIndexPath:idx];
return [[oneSession.objectID URIRepresentation] absoluteString];
}
And that’s all that’s needed to preserve the (scrolling) state.
Restoring state
This was more fun to figure out and it takes 3 steps:
- get
NSManagedObjectID
, usingmanagedObjectIDForURIRepresentation:
method ofNSPersistentStoreCoordinator
- use that to load actual
NSManagedObject
from theNSManagedObjectContext
usingexistingObjectWithID:error:
method - finally get
NSIndexPath
that we are after, fromNSFetchedResultsController
usingindexPathForObject:
Here’s the full method:
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view {
if (!identifier || !view) return nil;
// get the objectID first from PSC
NSManagedObjectID *objectID = [self.managedObjectContext.persistentStoreCoordinator managedObjectIDForURIRepresentation:[NSURL URLWithString:identifier]];
if (!objectID) return nil;
// then fetch the object from MOC
NSError *error = nil;
NSManagedObject *oneSession = [self.managedObjectContext existingObjectWithID:objectID error:&error];
if (error) {
NSLog(@"ERROR getting Session object from MOC:\n%@", [error localizedDescription]);
return nil;
}
if (!oneSession) return nil;
// finally get the index path from NSFRC
NSIndexPath *indexPath = [self.fetchedResultsController indexPathForObject:oneSession];
// iOS 6 bug workaround:
// Force a reload when table view is embedded in nav controller or scroll position is not restored.
[self.tableView reloadData];
return indexPath;
}
The workaround at the end is hopefully going away quickly, as we all move to iOS 7 only apps.