Bug Table View State Not Restored When Embedded In Navigation Controller

Sharing this bug report (rdar://13438788 duplicate of #12156999) for anybody attempting to get iOS 6 state preservation and restoration working for a table view embedded in a navigation controller.

Problem Description

Summary

Using iOS 6 state preservation and restoration the first visible row of a UITableView is not restored when the table view is embedded in a UINavigationController.

Steps to Reproduce

(using Xcode 4.6.1 and iOS 6.1.3)

  1. Create a new Xcode project using the iOS Single View Application template, specifying iPhone, Use Storyboards, Use Automatic Reference Counting.

  2. Enable state preservation and restoration in the App delegate by implementing application:shouldSaveApplicationState: and application:shouldRestoreApplicationState:. Both methods should return YES.

  3. Add a new class to the project that is a subclass of UITableViewController (without a XIB). Implement the two mandatory UITableViewDataSource delegate methods to provide a basic data source:

    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
      return 1000;
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
      static NSString *CellIdentifier = @"BasicCell";
      UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
      cell.textLabel.text = [NSString stringWithFormat:@"Cell #%d",indexPath.row];
      return cell;
    }
    
  4. Replace the storyboard contents with a single Table View Controller. Change the class of the controller to the name of the class created in step 3.

  5. In the storyboard set the Table View Cell style to Basic and set the cell reuse identifier to “BasicCell” as specified in step 3.

  6. Embed the table view controller in a navigation controller (Editor > Embed In > Navigation Controller).

  7. Set the Storyboard ID for the navigation controller and the table view controller and select the “Use Storyboard ID” option to use the same identifier for the restortation ID. Also set the restoration ID for the table view.

  8. Build and run

  9. Scroll down the table view until row 50 is the first visible row. Use the home button to move the App to the background and then use Xcode to stop the App.

  10. Run the app again to see the restored state.

Expected Results

The first visible row should be row 50 which was the first visible row when the App was terminated.

Actual Results

The first visible row is row 0.

Regression

Delete the navigation controller from the storyboard and repeat the test. On restoring the app the first visible row is row 50.

Notes

A sample Xcode project to reproduce the problem can be found in my GitHub Code Examples repository. The project contains two storyboards to demonstrate state restoration of a table view both when it is the root view and when it is embedded in a navigation controller:

  • If the Target settings are used to set the Main Storyboad to NavStoryboard the user interface consists of UITableView embedded in a Navigation Controller. In this use case the table view state is not restored.

  • If the Target settings are used to set the Main Storyboard to MainStoryboard the user interface consists of a single UITableView which does have state restored as expected.

Workarounds

Apple engineers suggested the following workaround which I can confirm resolves the issue:

Please do the following in the ViewController that owns the TableView:

- (void) encodeRestorableStateWithCoder:(NSCoder *)coder {
  // Save anything relevant for our role as the TableView's DataSource
  [super encodeRestorableStateWithCoder:coder];
}

- (void) decodeRestorableStateWithCoder:(NSCoder *)coder {
  [super decodeRestorableStateWithCoder:coder];
  // Restore whatever we need as the TableView's DataSource, and then...
  [self.tableView reloadData];
}

I have also found that implementing a similar workaround with the UIDataSourceModelAssociation protocol methods works:

- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view {
  // Determine identifier for the current index path
  NSString *identifier = ....
  return identifier;
}

- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view {
  // Determine index path for the identifier
  NSIndexPath *indexPath = ...

  // Force a reload when table view is embedded in nav controller
  // or scroll position is not restored
  [self.tableView reloadData];

  return indexPath;
}

The key to both workarounds is to force a reload of the table view data once the state has been restored.