Use Your Loaf

[[brain engage] write]

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.

Comments