Back in iOS 6, Apple added a set of APIs with specific aim to greatly increase user experience during app switching. That API is Application State Preservation and Restoration - you can learn about it using the official docs or WWDC 2012 Session 208 or using a series of great posts by Keith Harrison.
What’s common about these and few other resources on the web is that they lean on Storyboards and if you happen to not use them - like me in this particular case - then it’s not entirely clear where you do start. This API was confusing to me at first, but once I implemented it, it was really obvious how good this API is for people, in everyday usage. It’s worth the time.
Also, an important note: with iOS 7 and its outlined principles and human interface guidelines provided by Apple, this API became a whole lot more important. The transitions between the apps are much more natural and re-assuring if you have properly implemented this API.
In this post I’ll show you how I’ve implemented it in my running apps. They do not use storyboards and their basic structure is tab-bar controller with each tab containing a navigation controller with rich navigation stack inside.
App delegate code
You start from App delegate, where you most likely need to re-organize your code first.
House keeping first
Previously, all the code that setup global stuff needed for my app was in this familiar method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
You know, the usual stuff :).
Starting from iOS 6, there’s another method, very similar to that one:
application:willFinishLaunchingWithOptions:. This method is crucial for the state restoration to work properly. If you have been through the docs/tutorials, you have noticed that the order of business now is:
- process application state restore
So, to prepare your code, you now need to do this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
This is easy. Move entire data and hierarchy setup to
will method and leave
did method more or less empty.
Opt-in for state preserve/restore
Now, add these two methods to notify iOS that your app will preserve/restore its state. I copied the code in the first method entirely from Keith’s post:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
Ok, here comes the part that is done in the IB if you use storyboards. For each controller which state you want to preserve, you need to populate its
restorationIdentifier property. If you’re not using storyboards, this is how it’s done:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
With just the code above, you have already achieved something: you have preserved the selected tab. If I tap on 2nd tab (
Progress…) and exit the app, once I get back to it that tab will be automatically selected. This is UIKit doing the work for you - in case of
UITabBarController, it saves the tab selection state. UIKit does a whole lot more for you - again, go through the docs/videos to learn what.
Note that I’m populating only those controllers that are actually used in AppDelegate and are not referred to anywhere else. Meaning, I’m adding
UITabBarController and to each
UINavigationController but not to the top view controllers inside UINC.
I do that in their respective
init methods, as you will see now.
View controller stuff
Ok, now one step at the time. First, let’s add restoration identifiers to each view controller and to their views. That way, we are notifying UIKit to do its work.
Progress, About and Settings VCs are UITableViewController subclasses, so this code is enough for them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Reading the docs, I was lost for a bit which of the various state preservation paths / options I should use for each of the view controllers. Do I need restoration class for each one or not? After some trial and error, these are the rules:
- if view controller is pre-built, then you only need restorationIdentifier
- if VC is not pre-built, but it’s result of deliberate user action, then you need restoration class in order for it to be restored
By “pre-built”, I mean that it’s either loaded from storyboard or it’s always instantiated from code. In my case, all top level VCs are already created in
application:willFinishLaunchingWithOptions: so all I needed was the code above.
Ok, what about VCs loaded deeper in navigation stack?
When is restoration class needed
In About VC, tapping on some table cells loads a Help view controller, which is basically a container for web view into which I load an HTML file.
In order for this VC to be restored, this is the minimum required code. First, you need to opt-in with a new protocol in iOS 6, called
1 2 3
This protocol has only one class method:
1 2 3 4 5 6 7
However, if you leave it like this, restoration would not work. You need to assign both
1 2 3 4 5 6 7 8 9 10
You also need to set
restorationIdentifier to the view, either in viewDidLoad or in IB (since this VC has its .xib file, I did it there).
Now, when restoration starts,
viewControllerWithRestorationIdentifierPath:coder: will be called and help page would show up.
Or would it..?
Encoding VC properties required for state
Did you notice this comment:
1 2 3 4 5
In order for the page to load, Help VC needs to know the file path. In About VC, on tapping the table view cell, I do this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
As you can see, there are two properties that define the state:
So, we need to do two things.
- Save these two
- Restore them and assign to re-created VC
Here they are:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
This is a fairly simple VC, so it’s straight-forward what to do.
Proper restore of the view state
View state can mean many things. The most obvious thing people would notice is the scrolling offset - if you have a large table view and if you don’t restore it exactly where user left it scrolled, state restoration magic will be lost.
The simple solution would be to encode scrolling offset and restore it. But that’s a fragile attempt and very prone to errors, especially in data-driven views like table/collection views. So what Apple did is they provided an API to specifically deal with this case. Instead of scrolling offset, you remember what data items were visible and restore that. Even if you happen to change data source between the app sessions, if the originally visible item is still in the store, table view will be restored with that item visible at the same visual position.
First, you need to conform to new data protocol:
And then you implement its two methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
First method is the encoding part. When session preserving starts, UIKit will call it for visible cells. The string you return should somehow be connected to actual data source. In this simple table view, my data source is an array of strings - text labels shown in cells. They are all unique, so I can use them for encoding.
Second method will take that encoded string for each cell and return NSIndexPath that corresponds to that string. So you see, since we are not encoding old index paths we will be able to restore to the same cells regardless of what their new index paths are.
General view stuff
Ok, so this deals with scrolling. What about other stuff that influence the view state?
For example, in Progress VC, I have a button, next to each section title, to reveal summary for each run. This is the kind of stuff that are result of user action and thus need to be preserved and restored. There are two new UIView APIs just for this; we already used one before.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
I use NSMutableSet to keep track of indexes of sections where summaries are shown. This set is encoded during preservation run and then restored when table view is loaded. Easy peasy.
Believe it or not - that’s it. All you needed to do. Make sure that you never even attempt to encode/decode data models. Think about what’s required for both the controller and its view to appear and encode that. Re-use as much of your existing code and view lifecycle as possible.
In upcoming post, I will demonstrate more advanced stuff - how to deal with VC where data model is Core Data based (like mentioned Progress VC) and what to do with deeper VC that are also based on Core Data. Like for example RunDetails VC which is loaded when you tap on cell representing completed run in Progress table view and it’s given a
NSManagedObject as data source.