Use Your Loaf

[[brain engage] write]

Restoration Classes and UIWebViews

I previously covered the basics of using state preservation and restoration but for the sake of brevity I did not provide an example of how to use a Restoration Class. This post will fix that omission and also take a look at how you can implement state restoration for a UIWebView.

A Quick Recap

If you need a recap on the basics of state preservation and restoration you should refer back to the previous post. In brief though the key points are as follows:

  • The Application Delegate must opt-in
  • All View Controllers and Views to be preserved/restored must have a restoration identifier.
  • View Controllers which are part of the main storyboard or Nib file will be created on restoration as part of the application launch.
  • View Controllers which are not otherwise created on application launch should either implement a restoration class or rely on the Application Delegate to create them.
  • For restored view controllers implement encodeRestorableStateWithCoder: and decodeRestorableStateWithCoder: where necessary to also restore the state of the view controller.

The rest of this post is going to examine the situation where we need a restoration class to restore a view controller by building on the example App I introduced in the last post.

Using A Restoration Class

Up to now all of the view controllers in our sample App are contained in the main storyboard. This means that once we have assigned restoration identifiers they will all get restored with the Storyboard on application launch.

To illustrate the use of a restoration class to restore a view controller that is not loaded as part of the storyboard I have added a button to the settings screen which can be used to display a web view:

The action method invoked by the button creates a new view controller from a Nib file which is used to show the web view. The view controller is pushed onto the existing navigation controller stack and an initial page is loaded:

- (IBAction)pushMe
{
  UYLWebViewController *wvc = [[UYLWebViewController alloc] initWithNibName:@"UYLWebViewController" bundle:nil];
  wvc.restorationIdentifier = @"UYLWebViewController";
  wvc.restorationClass = [UYLWebViewController class];
  [self.navigationController pushViewController:wvc animated:YES];
  [wvc showPage:@"http://useyourloaf.com"];
}

Note that we set the restoration identifier of the view controller to indicate that we want it restored. We also need to set the restoration class that will take care of creating the new instance of the controller during a restore - it is common to have a class act as its own restoration class. We will see how to implement the restoration class in a moment but we also want to restore the state of the web view. Luckily the UIWebView class already takes care of restoring itself, the details are in the UIKit documentation:

In iOS 6 and later, if you assign a value to this view’s restorationIdentifier property, it attempts to preserve its URL history, the scaling and scrolling positions for each page, and information about which page is currently being viewed. During restoration, the view restores these values so that the web content appears just as it did before.

We can set the restoration identifier of the UIWebView in the Nib file:

To implement the restoration class we need the web view controller to adopt the UIViewControllerRestoration protocol:

@interface UYLWebViewController () <UIViewControllerRestoration>

We then need to implement the class method to actually create the new instance of a view controller:

+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
  UIViewController *viewController = [[UYLWebViewController alloc] initWithNibName:@"UYLWebViewController" bundle:nil];
  viewController.restorationIdentifier = [identifierComponents lastObject];
  viewController.restorationClass = [UYLWebViewController class];
  return viewController;
}

Note that we need to remember to set the restorationIdentifier and restorationClass when we create the restored view controller. You can get the restorationIdentifier from the last object of the array of identifier components.

Now during application restoration when UIKit encounters the restoration identifier of this web view controller it will invoke the restoration class method to create a new instance of the class. Note that if we have view controller state to restore we would do that by implementing the encodeRestorableStateWithCoder: and decodeRestorableStateWithCoder: methods.

At this point you can build and run the app, navigate to the settings tab and use the button to display the web view. If you background the App and use Xcode to stop and then relaunch you should see the web view restored. However what I find actually happens is that the web view appears to be blank. I am not sure if this intended behaviour but it seems to require that the web view is reloaded after restoring the state.

Restoring the State of a UIWebView

What we need to do is force a reload of the web view when we are restoring state. One way we can do that is to introduce an instance variable to indicate when we are performing a restoration:

@property (nonatomic) BOOL restoringState;

We can then implement decodeRestorableStateWithCoder: to flag when we are doing a restore (remembering that we always need to call super):

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
  [super decodeRestorableStateWithCoder:coder];
  self.restoringState = YES;
}

Now we can test for a restore in viewDidAppear: and force the reload:

- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
  if (self.restoringState)
  {
    [self.webView reload];
    self.restoringState = NO;
  }
}

Now when you test restoration of the web view you should find that the URL, scaling and scrolling positions are indeed restored.

Wrapping Up

As I play more with state restoration I am finding that getting the basics to work is pretty straightforward. However as in this example with a UIWebView and as we saw previously with table views embedded in a navigation controller there are a few rough edges. You can find the updated version of the Restorer Xcode project in my GitHub CodeExamples repository.

State Preservation and Restoration

From the very first releases of the iPhone SDK Apple has encouraged developers to think about app startup and switching to make the experience as quick and transparent as possible for the user. The limited resources of mobile devices mean that App termination is a common occurrence. Returning to a previously running App that has been terminated by the system and finding it back at a startup screen is not a great user experience. The ability for an App to be suspended and resumed was introduced with iOS 4 and helps to reduce the problem but to make App termination transparent to the end user still takes developer effort.

There is a non-trivial amount of work required to save and then restore a deeply nested hierarchy of views and view controllers. Luckily with iOS 6 direct UIKit support for state preservation and restoration was introduced. This post is a collection of my notes on the basic steps to implement state preservation and restoration.

State Restoration Flowchart

A key point to understand about the way UIKit implements state preservation is that it happens when the App moves to the background. You indicate to UIKit which view controllers and views you want to preserve by assigning them restoration identifiers and UIKit then does the hard work of saving the necessary data to an archive file. If the App is then terminated by the system whilst it is suspended the saved archive can be used to restore the App state the next time it is launched.

The state restoration process is controlled by a series of interactions between UIKit, the Application Delegate and the preserved view controllers and views as summarised in the following flowchart:

State Restoration

A Simple Example

As always it is easier to show the key concepts by way of example so here is a very simple example application with only a few UI elements. The main user interface is a tab-bar controller with two tab-bar items. The first tab is a table view controller embedded in a navigation controller. The table view shows a list of countries, selecting a row navigates to a detailed view controller showing details about the country. The second tab is a settings view containing a single UISwitch.

The following application state should be restored when the App is launched:

  • The selected tab in the tab-bar should be restored.
  • Scroll position and any selection of the table view should be restored
  • Navigation hierarchy of the Navigation Controller (the table view or detailed view may be visible).

This is a good point to differentiate between an applications data model and user interface state. The state preservation and restoration system is intended to take care of the user interface state. As we will see shortly the state preservation data will be deleted if the user force-quits the App or if the restoration process fails. You should therefore NOT attempt to use it to store the applications data model. The data model is best stored to a persistent data store for example by writing it to a file, database or something like Core Data. When a view is restored it can of course make use of the persistent data model to correctly configure the restored view.

In this simple example the table view data source is loaded from a plist file and the current state of the settings are saved to the user defaults (using NSUserDefaults). The state of the switch is initialised when the settings view is loaded and saved any time it changes:

- (void)viewDidLoad
{
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  self.amazingSwitch.on = [defaults boolForKey:kUYLSettingsAmazingKey];
}

- (IBAction)amazingAction
{
  BOOL amazingEnabled = self.amazingSwitch.isOn;
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  [defaults setBool:amazingEnabled forKey:kUYLSettingsAmazingKey];
  [defaults synchronize];
}

Changes To The Application Delegate Launch Process

If you refer back to the earlier flowchart you will see that with iOS 6 there are now two methods available in the application delegate to initialise the App post launch:

  • application:willFinishLaunchingWithOptions:
  • application:didFinishLaunchingWithOptions:

The major difference between these two methods is that the “willFinish” method, which is new in iOS 6, is called before state restoration is performed. The “didFinish” method is called after any state restoration has occurred. As a general guideline you will want to perform most application initialisation before state restoration takes place so that things such as the data model are available when the view controllers are restored.

If you have an existing App that has initialisation code in application:didFinishLaunchingWithOptions: you can mostly just move this code to the new application:willFinishLaunchingWithOptions: method. However you need to be careful if you want backward compatibility to iOS 5 or earlier versions as the “willFinish” method will never be invoked. One approach is to move everything to a common initialisation method and invoke it from both methods. You can further wrap the initialisation code in a dispatch_once block if you need to ensure the initialisation is executed only once:

- (void)commonFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
                                 [NSNumber numberWithBool:YES], kUYLSettingsAmazingKey,
                                 nil];
    [[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];
  });
}

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  [self commonFinishLaunchingWithOptions:launchOptions];
  return YES;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  [self commonFinishLaunchingWithOptions:launchOptions];
  return YES;
}

If you do not care about backward compatibility with iOS 5 you can avoid this extra work and directly place your initialisation code in the appropriate app delegate method.

Basic Steps to Enable State Preservation

To get started with state preservation and restoration there are two basic steps that are always required:

  • The App delegate must opt-in
  • Each view controller or view to be preserved/restored must have a restoration identifier assigned.

In addition to these mandatory steps there are also a number of optional but very common steps:

  • Create restoration classes for any view controllers that need to be recreated during a restore that are not otherwise created on App launch.
  • Implement encodeRestorableStateWithCoder: and decodeRestorableStateWithCoder: for views and view controllers that require state to be saved and restored.
  • Implement the UIDataSourceModelAssociation protocol for objects that are data sources for table and collection views to save and restore which rows are visible and which are selected.

Application Delegate Opt-in

State preservation and restoration is an optional feature so you need to have the application delegate opt-in by implementing two methods:

- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder
{
  return YES;
}

- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder
{
  return YES;
}

Most of the time you just want to return YES to indicate support for state preservation and restoration. However if you release an App upgrade where it would not make sense to restore from a previous version you can return NO from application:shouldRestoreApplicationState:. This might be the case if you have completely changed the view hierarchy for example.

Restoration Identifier

A restoration identifier is a string that you need to assign to any view controller or view that you want preserved and restored. During state preservation any view controllers or views in the view hierarchy that have a restoration identifier will be saved to disk.

The restorationIdentifier property can be set when the view controller is initialised but if you are loading the view controller from a Nib or Storyboard you can also set it there. You can choose any value you want as the identifier but you must ensure that the restoration path (the sequence of restoration identifiers down the view hierarchy from the root view controller) is unique for each object to be restored.

For our simple example project we can set the restoration identifiers in the Storyboard. For view controllers you can use Interface Builder to set a Storyboard ID and by selecting the option reuse the same ID for the Restoration ID:

Restoration identifiers need to be set for the following view controllers:

  • The Tab Bar Controller
  • Both Navigation Controllers
  • The Table View Controller and the associated UITableView
  • The Settings View Controller

An easy mistake to make when using Interface Builder is to set the restoration identifier on the wrong object. For example, be sure to set the restoration identifier on the table view itself and not on the table view cell. The following screenshot of the storyboard shows the identifiers I used for each of the view controllers:

Finding or Creating View Controllers During Restoration

During the restoration process UIKit will attempt to create or locate a reference to each view controller that is being restored. If the main user interface is loaded from a Nib file or Storyboard then it will already have been created when the application launched. In that case UIKit will locate the already created view controller instead of creating a new instance.

In this simple example all of the view controllers to be restored are located in the Storyboard file that is loaded when the application launches. This means we have nothing else to do as UIKit will take care of recreating the view controllers for us (we still need to restore the state of the view controllers). For view controllers that are not created as part of the normal application launch UIKit attempts the following steps:

  • If the view controller has a restoration class (it implements the UIViewControllerRestoration protocol) then UIKit will use it to call the viewControllerWithRestorationIdentiferPath:coder: method. By implementing that method you can return an instance of the view controller.
  • If the view controller does not have a restoration class UIKit will call application:viewControllerWithRestorationIdentifierPath:coder: in the application delegate.

I will save a detailed discussion of using a restoration class to a future post.

Restoring State of View Controllers and their Views

Once all of the view controllers we need have been created we need to restore any state they may need to properly recreate the user interface and any associated subviews they may contain. For any object where we have set a restoration identifier we have the option to save and restore state information by implementing the following two methods:

  • encodeRestorableStateWithCoder:
  • decodeRestorableStateWithCoder:

In our example App we have a view controller (UYLCountryViewController) that is used to display a view with the name of the capital city of the country selected in the root table view controller. The data model for this view controller is just an NSString property which is set when the view controller is pushed onto the navigation stack:

@property (nonatomic, copy) NSString *capital;

When this view controller is restored we need to ensure that we also restore this data. In this case the encode and decode methods that we need to implement are as follows:

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
  [coder encodeObject:self.capital forKey:UYLKeyCapital];
  [super encodeRestorableStateWithCoder:coder];
}

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
  self.capital = [coder decodeObjectForKey:UYLKeyCapital];
  [super decodeRestorableStateWithCoder:coder];
}

Note that in both cases you need to call super to ensure the parent view controller gets a chance to save and restore state.

Preserving Table View State

For the table view showing the list of countries we want to be able save the current selection and the scroll position. Since the data source can change between launches of an application this is a little more complicated than just saving the indexPath of the selected and first visible row. Relying on the indexPath will give us wrong results if when we restore a number of rows have been deleted or added to the table.

What we need to do is map between the indexPath and an identifier we can use to uniquely locate data items. To do that we need to adopt the UIDataSourceModelAssociation protocol:

@interface UYLTableViewController () <UIDataSourceModelAssociation>

This protocol has two required methods to map between an index path of an object in the table and an NSString identifier:

  • modelIdentifierForElementAtIndexPath:inView:
  • indexPathForElementWithModelIdentifier:inView:

In this simple example we can use the country name to uniquely identify an object in the table. For a Core Data backed data model a good choice would be a string representation of the managed object ID. The method used when saving the state which maps an index path to a string identifier is straightforward:

- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view
{
  NSString *identifier = nil;
  if (indexPath && view)
  {
    NSDictionary *country = [self.worldData objectAtIndex:indexPath.row];
    identifier = [country valueForKey:@"capital"];
  }
  return identifier;
}

The method used on restoration needs to map back to an index path for the object based on the identifier. This is a little more complicated in this case as we need to search an array of dictionaries for the matching object. Luckily we have a way to search an array using an NSPredicate and a block:

- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view
{
  NSIndexPath *indexPath = nil;
  if (identifier && view)
  {
    NSPredicate *capitalPred = [NSPredicate predicateWithFormat:@"capital == %@", identifier];
    NSInteger row = [self.worldData indexOfObjectPassingTest:
                     ^(id obj, NSUInteger idx, BOOL *stop)
                     {
                       return [capitalPred evaluateWithObject:obj];
                     }];

    if (row != NSNotFound)
    {
      indexPath = [NSIndexPath indexPathForRow:row inSection:0];
    }
  }

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

  return indexPath;
}

Note that in the case that we cannot find the object because it has been deleted from the dataset we return nil. Also note that because of a bug in iOS 6 we need to force a reload of the table view or the scroll position is not restored. See this previous post about the table view state not being restored when embedded in a navigation controller or also see rdar://13438788 for more details.

Testing

At this point we have done everything we need to save and restore state for this simple App. We can test the results using either the simulator or a device but you need to remember a key point I mentioned earlier. The state is only saved when the App is moved to the background and will be deleted if you force quit the App. The easiest way to test state restoration using the simulator is with the following procedure:

  • Launch the App in the simulator and navigate to create the required App state.
  • Use the home button (⇧⌘H) to send the App to the background.
  • Stop the App using Xcode.

At this point we have an App that is not running but has some saved background state. To test that the state is restored you can relaunch the App directly in the Simulator or by running it again from Xcode. A common mistake I find myself making when testing is to stop the App before sending it to the background.

Wrapping Up

This has been a long post considering that I only covered the basics but hopefully you have managed to follow along. I do think that for many simple use cases it is pretty simple to implement as UIKit really does take care of most of the work. I will try to explore some of the more advanced topics such as the use of restoration classes in a future post. In the mean time you can find the example Restorer XCode project in my GitHub CodeExamples repository.

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.