First thing to do is setup Restoration ID's for your view controllers. You can set this up in Xcode or programatically via the restorationIdentifier property of UIViewController
An important thing to remember is that with storyboards every controller down the restoration chain must have a restorationID for this to work. The name itself is unimportant except you will want  a unique name per controller. Any gaps in the chain will cause restoration to fail. 
Next job is to set up delegate methods in your UIApplicationDelegate
    
func application(application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
    return true
}
    
func application(application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
    return true
}
This asks two questions of your application. Do you want to save state on exiting active? and Do you want to restore state at app launch? We will assume yes to both things but there may be cases in real life where this isn't true.
Every UIViewController in the chain will now receive the following message on save
 
override func encodeRestorableStateWithCoder(coder: NSCoder) 
And the corresponding message during restoration
 
override func decodeRestorableStateWithCoder(coder: NSCoder) 
Its recommended that you call super to allow any classes up the chain to perform restore operations.
These coders allow you to archive bits of information which will allow you to restore that UIViewController instance to the state it was when the app exited.
TIP: When debugging this the save sequence will only occur when the app is sent to background so you will need to trigger a home button action in the sim with CMD-SHIFT-H
These coders are used in the standard way so you can encode any standard Foundation classes. More complex things may need to be stored as NSData
 
override func encodeRestorableStateWithCoder(coder: NSCoder) {
    super.encodeRestorableStateWithCoder(coder)
    coder.encodeObject(thingIWantToRemember,forKey:"importantString")
    coder.encodeBool(switchstate,forKey:"switchState")
}
And the corresponding message during restoration
 
override func decodeRestorableStateWithCoder(coder: NSCoder) {
    super.decodeRestorableStateWithCoder(coder)
    if let string = coder.decodeObjectForKey("importantString") as? String {
        self.configureUIWithString(string)
    }
    if let switchstate = coder.decodeBoolForKey("switchState") {
        self.configureSwitchUI(switchstate)
    }
}
Remember you should not be recording any important data here. Just enough fragments to restore your UI in this view controller. The OS does not guarantee that state will be saved forever or that your state will be ever be restored in the future.
There is a third method which is called on the UIViewController at the end of the restore sequence.
 
optional func applicationFinishedRestoringState()
This is where you can do restoration things which can only be done at the end of the sequence. For example where you have upstream dependancies on your view controller stack.
When it comes time to restore there is an UIApplicationDelegate method which will be called to instantiate each of the view controllers in the restoration chain.
 
func application(application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [AnyObject], coder: NSCoder) -> UIViewController?
unless you set the restorationClass property.
This allows you to set a class which will handle instantiation of your view controller. In which case the following class function call is made.
 
class func viewControllerWithRestorationIdentifierPath(identifierComponents: [AnyObject], coder coder: NSCoder) -> UIViewController?
This is described fully in the UIViewControllerRestoration documentation.
For example if you want to allow the view controller to handle its own re-instantiation.
class MyViewController:UIViewController {
   required init(coder aDecoder: NSCoder) {
      super.init(coder: aDecoder)
      self.restorationClass = MyViewController.self    
   }
...
Lets consider the case with a simple TableView/DetailView where you allow the App Delegate to handle all instantiation during restoration.
func application(application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [AnyObject], coder: NSCoder) -> UIViewController? { let storyboard = UIStoryboard(name: "Main", bundle: nil); if identifierComponents.count > 1 { if let lastItem = identifierComponents.last as? String { return storyboard.instantiateViewControllerWithIdentifier(lastItem) as? UIViewController } } return nil }
- This method will get called three times. Once per view controller.
 - Each time the identifierComponents array will carry the restorationID of the view controller plus the others below it. By the time the third controller is restored the array will have 3 items in it.
 
Notice how Im not allowing the root navigation container to instantiate by requiring there to be at least two components in the stack. If you don't do this the navigation controller will instantiate and place its root controller in the stack. Two for the price of one, but we don't want that so we block it from forming. Don't worry, it works out in the end. 
Once you have done all these things, next time your app starts cold iOS will place an image snapshot of the the last time you exited into view and start restoring. If you get it right there will be a slight flicker as the snapshot image cross fades into the live view controller and it will be as if your user never left. 
In Summary 
- Give your view controllers a restorationID. Don't break the storyboard chain
 - Implement the app delegate methods to tell iOS that you want to participate in the store/restore
 - Manage state storage/restoration of each view controller in encodeRestorableStateWithCoder/decodeRestorableStateWithCoder
 - Handle re-instantiation in viewControllerWithRestorationIdentifierPath
 
Traps 
- Your view controllers will receive viewDidLoad but won't receive viewWill/DidAppear unless its the top of the stack.
 - If your stack is rooted on a container controller e.g UINavigationController / UITabController/UISplitViewController , when a child controller receives viewDidLoad the container will be NOT be bound to its child. So for example calling self.navigationController/self.tabBarController/self.splitViewController will return nil.
 
The app delegate has two other methods which can tune your store/restore cycle
func application(application: UIApplication, willEncodeRestorableStateWithCoder coder: NSCoder) 
    
func application(application: UIApplication, didDecodeRestorableStateWithCoder coder: NSCoder) 
These are called prior to the stack being stored and after the stack is restored respectively.
Check out other techmo stuff in UIStateRestoring documentation


No comments:
Post a Comment