Aleksandar • Vacić

iOS bits and pieces

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.

Runs progress

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:

1
@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).

1
2
3
4
5
6
7
8
9
- (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:

1
2
3
4
5
6
7
- (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, using managedObjectIDForURIRepresentation: method of NSPersistentStoreCoordinator
  • use that to load actual NSManagedObject from the NSManagedObjectContext using existingObjectWithID:error: method
  • finally get NSIndexPath that we are after, from NSFetchedResultsController using indexPathForObject:

Here’s the full method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (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.