State Restoration in iOS App

February 3, 2016

A user is more likely to take a break from your app in the middle of its usage. And, at some point, the operating system may discard your app from memory; this leads to fresh launch when user returns. Meh, user is unhappy for loosing his work flow! State Restoration is your knight in shining armor. It enhances the user experience by preserving configuration of your app before it is suspended so that the configuration can be restored on a subsequent launch.

Context Hierarchy

Assume you have navigated through interesting choices on a shopping app and decided to continue checking out choices later tonight, or constructed an important message/tweet/note, or filled out a form partially and pushed the app to background to do it later; certainly, you wouldn’t be happy to start over again when you are back. These are some examples of use cases for State Restoration feature. So, you think this user experience could play significant presence in enterprise/utility apps? You bet!

Alright, let’s go ahead to implementation. The steps to implement are follows:

Step (1):

Enable state restoration by returning true to following application delegate methods. Mission is ON!

func application(application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
      return true
  }
func application(application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
     return true
  }
Step (2):

Each View Controller has a property called restorationIdentifier (default: nil), assign a unique string value to it. This will tell which controllers and views should be preserved and restored. There are two ways of doing it. You could set this in storyboard under the tab 'Identity Inspector' or you could do it in viewDidLoad() right after calling super.

Tip: View controllers already have a ''Storyboard ID'' and you could simply use the same string.

Restoration Paths: How does system remembers path of navigation, you ask? Restoration identifiers are used to form a unique path to each view controller. For example, to reach 'ProfileViewController', restoration identifiers are used as "RootViewController/NavigationController/ProfileViewController”.

Programmatically Created View Controllers:

If you have chosen programatic way to create View Controller instead of storyboard/Xib, there isn’t much different here. You will have to do the same changes in viewDidLoad() instead.

override func viewDidLoad() {
  super.viewDidLoad()

  // Tells unique restoration Identifier
  restorationIdentifier = "ProfileViewController"

  // Tells UIKit which class to instantiate for restoration
  restorationClass = PetEditViewController.self
}

Now, the restoration view controller class must conform to protocol UIViewControllerRestoration system and return an instance of same view controller using the required method. Implementation goes like:

extension ProfileViewController: UIViewControllerRestoration {
  static func viewControllerWithRestorationIdentifierPath(identifierComponents: [AnyObject],
  coder: NSCoder) -> UIViewController? {
  let vc = ProfileViewController()
  return vc
  }
}

If you build and run at this point, you wouldn’t find the data you are looking for. Continue to step 3.

Step (3):

Store (encode) and Retrieve (decode) any configuration data necessary to reconstruct view controller to its original state. You do this by implementing UIStateRestoring protocol.

In view controller, add following code to override encodeRestorableStateWithCoder(_:) and decodeRestorableStateWithCoder(_:)

Encode:

override func encodeRestorableStateWithCoder(coder: NSCoder) {

  // preserve only ID of Profile Model Object.
  if let profileID = profileID {
      coder.encodeInteger(profileID, forKey: "profileID")
  }

  // save image
  if let image = profileImageView.image {
      coder.encodeObject(UIImagePNGRepresentation(image), forKey: "image")
  }

  //save string
  if let name = nameTextField.text {
    coder.encodeObject(name, forKey: "name")
  }

  super.encodeRestorableStateWithCoder(coder)
}

Decode:

override func decodeRestorableStateWithCoder(coder: NSCoder) {

  // retrieve profile ID so that we can fetch entire profile
  profileID = Int(coder.decodeIntegerForKey("profileID"))

  //image data
  if let imageData = coder.decodeObjectForKey("image") as? NSData {
    profileImageView.image = UIImage(data: imageData)
  }
  // name
  if let name = coder.decodeObjectForKey("name") as? String {
    nameTextField.text = name
  }

  super.decodeRestorableStateWithCoder(coder)
}

Get Profile:
The UIStateRestoring protocol provides, applicationFinishedRestoringState() for additional configurations after decoding stored object(s).

override func applicationFinishedRestoringState() {
  // ask data manager for Profile
  guard let profileID = profileID else { return }
  myProfile = DataManager.sharedManager.profileWithID(profileID)
}

That’s it, Amigos!
Here is the sample code from Apple on state restoration - Download here.

Note: State restoration doesn’t works when app is killed via app switcher. Also when state restoration fails with some error, off course.

Now, send out a message - "This is you. You have an iOS App. You support State Restoration. You Rock!"