Once iOS is given the green light to save your view state, its starts with the root view controller being displayed and checks for a restoration ID. A restoration ID is a string property (restorationIdentifier) used to tag the stateinformation for that view controller. It also acts as a flag, inviting iOS to preserve, and
ultimately restore, that view controller’s state. If the restorationIdentifer property is nil, iOS ignores the view controller; nothing gets persevered, and nothing will be restored.
iOS then looks for any view (UIView) objects that have a restorationIdentifier set, and preserves them. If the root view controller is a container view controller, the entire process repeats with each sub-view controller, capturing thestate of each view controller with a restoration ID and ignoring those without.
You can set restoration IDs programmatically, but if your view controller is defined in an Interface Builder file it’s simplest to set them there. Select the Main_iPhone.storyboard (or _iPad) file. Select the root tab bar view controllerand switch to the identity inspector, as shown in Figure 18-2.
Locate the Restoration ID property and set it to TabViewController.
Figure 18-2. Setting restoration ID property
You’ve now done everything required to get iOS to save, and restore, that state of your tab view controller. This, unfortunately, won’t do you much good. What you want is the sub-view controller that was visible when the userquit Wonderland to reappear when they launch it again. For that to happen, each of the sub-view controllers must be restored too. Using the identity inspector, select each of the sub-view controllers and assign themrestoration IDs too, using Table 18-1 as a guide.
Table 18-1. Wonderland view controller restoration IDs View
Controller Restoration
ID Root Tab View Controller TabViewController
WLFirstViewController CoverViewController
UINavigationController CharacterNavController
WLBookViewController BookViewController
This is enough to remember, and later restore, the top-level tab the user was viewing when they quit the app. Give it a try:
1. Run the Wonderland app
2. Choose the character or book tab
3. Press the home button to push the app into the background
4. Stop the app in Xcode
5. Run the app again
The restoration ID strings can be anything you want; they just have to be unique within the scope of the other view controllers.
Customizing Restoration
So far, the only view state that gets restored is which tab the user was in. If they were viewing a
character’s information, or had thumbed through to page 87 of the book, they’ll return to the character
list and page 1 when the app is relaunched.
Deciding how much view state information to preserve is up to you. As a rule, users expect to return to whatever they were doing when they quit the app. But there are limits to this. If the user had entered a modal view controllerto pick a song or enter a password, it wouldn’t necessarily make sense to return them to
that exact same view two days later. You’ll have to decide how “deep” your restoration logic extends.
For Wonderland, you definitely want the user to be on the same page of the book. Your users would be very annoyed if they had to flip through 86 pages to get back to where they were reading yesterday. The page view controller, however, knows nothing about the organization of your book data. That’s something you created when you wrote the WLBookDataSource class. If you want to preserve and restore the page they were on, you’ll have towrite some code to do that.
Each view and view controller object with a restoration ID receives an
-encodeRestorableStateWithCoder: message when the app moves to the background. During application startup, it receives a -decodeRestorableStateWithCoder: message to restore itself. If you want to preserve custom stateinformation, override these methods.
Select the WLBookViewController.m implementation file. Add these two methods to the
@implementation section:
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[super encodeRestorableStateWithCoder:coder]; WLOnePageViewController *currentView = self.viewControllers[0]; [coder encodeInteger:currentView.pageNumber forKey:@"page"];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
[super decodeRestorableStateWithCoder:coder]; NSUInteger page = [coder decodeIntegerForKey:@"page"]; if (page!=0)
{
WLOnePageViewController *currentView = self.viewControllers[0]; currentView.pageNumber = page;
}
}
The first method obtains the current view controller being displayed in the page view controller.
The WLOnePageViewController knows which page number it’s displaying. This number is saved in the
NSCoder object. When your app is relaunched, the page view controller receives a
decodeRestorableStateWithCoder: message. It looks inside the NSCoder
object to see if it contains a saved page number. If it does, it restores the page number before the view appears, returning the user to where they were when they quit. That wasn’t too hard, was it?
Test out your new code. Launch Wonderland, flip through a few pages of the book, then quit the app and stop it in Xcode. Launch it again, and the last page you were looking at will reappear, as if you’d never left.
Deeper Restoration
Exactly how much view state information you decide to preserve is up to you. Here are some tips to developing a restoration strategy:
n UIView objects can be preserved too. Assign them a restoration ID and, if necessary, implement -encodeRestorableStateWithCoder: and-decodeRestorableStateWithCoder: methods.
n If you want to restore the state of a data model for a table or collection view,
your data source object should adopt the UIDataSourceModelAssociation protocol. You then implement two methods (-indexPathForElementWithModelId entifier:inView: and -modelIdentifierForElementAtIndexPath:inView:) that remember, and restore, the user’s position in the table.
n You can encode and restore
anything you want in your app delegate’s
application:shouldSaveApplicationState: and application:shouldRestore ApplicationState: methods.
You can use these methods to perform your own view controller restoration, or use a combination of theautomatic restoration and a custom solution
The gory details are all explained in the “State Preservation and Restoration” chapter of the iOS App Programming Guide, which you can find in Xcode’s Documentation and API Reference window.
Pigeons in the Cloud
Cloud storage and synchronization are hot new technologies that make iOS devices even more useful.
Set an appointment on one, and it automatically appears on all of your other devices. The technology behind this bit ofmagic is complex, but iOS makes it easy for your app to take advantage of it.
There are a number of cloud storage and synchronization features in iOS, but the easiest to use, by far, is the NSUbiquitousKeyValueStore object. It works almost identically to user defaults. The difference is that anything youstore there is automatically synchronized with all of your other iOS devices. Wow!
There are both practical limits and policy restrictions on what information you should, or can, synchronize between devices. Your first task is to decide what it makes sense to share. Typically, user settings and view statesare only preserved locally. It would be weird to change the map type on your iPhone, and then suddenly have your iPad’s map view change too. On the other hand, if your user was reading Alice’s Adventures in Wonderland ontheir iPad, wouldn’t it be magic if they could reach for their iPhone and open it up at the same page?
Another reason to carefully choose what you synchronize is that the iCloud service strictly limits how much information you can share through NSUbiquitousKeyValueStore.The limits are:
n No more than 1MB of data, in total
n No more than 1,000 objects
n A “reasonable” number of updates
Apple doesn’t spell out exactly what “reasonable” is, but it’s a good idea to keep the number of changes you make to NSUbiquitousKeyValueStore to a minimum.
Storing Values in the Cloud
Let your Pigeon app spread its wings by adding cloud synchronization. The only piece of information you’ll synchronize is the remembered map location—the map type and tracking mode aren’t good candidates for syncing. Youuse NSUbiquitousKeyValueStore almost exactly the way you use NSUserDefaults. In fact, they are so similar that you’ll be reusing many of the same strategies and methods you wrote at the beginning of this chapter.
You get a reference to the singleton NSUbiquitousKeyValueStore object via [NSUbiquitousKeyValueStore defaultStore]. Any values you set are automatically serialized and synchronized with the iCloud servers.
Select HPViewController.m and add an instance variable to the private @interface HPViewController (). This will retain the cloud store object (new code in bold):
@interface HPViewController () <UIAlertViewDelegate>
{
MKPointAnnotation *savedAnnotation; UIImageView *arrowView; NSUbiquitousKeyValueStore *cloudStore;
}
Initialize the new variable by adding these statements to the end of the –viewDidLoad method:
cloudStore = [NSUbiquitousKeyValueStore defaultStore]; [cloudStore synchronize];
This code retrieves and saves a reference to the singleton cloud store object, and then requests an
immediate synchronization. This prompts iOS to update any values in the store that might have been
changed by other iOSdevices,and vice versa. It will happen eventually, but this hurries the process along when the app first starts, and is the only time you’ll need to send -synchronize.
Now update the -preserveAnnotation method so it stores the annotation information in both the user defaults and the cloud (new code in bold):
- (void)preserveAnnotation
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; if (savedAnnotation!=nil)
{
NSDictionary *annotationInfo = [savedAnnotation preserveState]; [userDefaults setObject:annotationInfo
forKey:kPreferenceSavedLocation];
[cloudStore setDictionary:annotationInfo forKey:kPreferenceSavedLocation];
} else
{
[userDefaults removeObjectForKey:kPreferenceSavedLocation];
[cloudStore removeObjectForKey:kPreferenceSavedLocation];
}
}
Cloud Watching
Unlike user defaults, the values in the cloud can change at any time. So it’s insufficient to simply read them when your app starts. Your app has to be prepared to react to changes, whenever they occur. In addition, your iOSdevice doesn’t always have access to the cloud. It may be in “airplane” mode, experiencing spotty cell reception, or maybe you’re using your device inside a Faraday cage—for a little privacy. No matter what, your app shouldcontinue to work in an intelligent manner under all of these conditions.
The preferred solution is to mirror your cloud settings in your local user defaults. This is what
-preserveAnnotation does. Whenever the location changes, both the user defaults and the cloud
are updated with the same value. If the cloud can’t be updated just now, that won’t interfere with the app. Likewise, if a value in the cloud changes, you should update your user defaults to match.
Which brings you to the task of observing changes in the cloud. So how do you find out when something in the cloud changes? At this point in the book, you should be chanting “notification, notification, notification,”because that’s exactly how you observe these changes. Your view controller observes the NSUbiquitousKeyValueStoreDidChangeExternallyNotification notification (which is also the runner up for being the longest notificationname in iOS). You’ll create a
new method to process those changes, so begin by adding that to the private @interface HPViewController () section in HPViewController.m:
- (void)cloudStoreChanged:(NSNotification*)notification;
Find the -viewDidLoad method and augment the code that sets up the cloud store (new code in bold):
cloudStore = [NSUbiquitousKeyValueStore defaultStore]; NSNotificationCenter
*center = [NSNotificationCenter defaultCenter]; [center addObserver:self
selector:@selector(cloudStoreChanged:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object:cloudStore];[cloudStore synchronize];
The -cloudStoreChanged: message will now be received whenever something in the cloud changes.
The last step is to write that method:
- (void)cloudStoreChanged:(NSNotification*)notification
{
NSDictionary *cloudInfo = [cloudStore dictionaryForKey:
kPreferenceSavedLocation]; NSUserDefaults *localStore = [NSUserDefaults standardUserDefaults]; [localStore setObject:cloudInfo forKey:kPreferenceSavedLocation]; [self restoreAnnotation];
}
Whenever the cloud values change—and there’s only one value, so you don’t even need to worry about which one changed—it retrieves the new value and copies it into the local user defaults. It then sends -restoreAnnotation torestore the map location from the user defaults, which is now the same as the value in the cloud.
Between -preserveAnnotation and -cloudStoreChanged:, the user defaults always has the latest (known) location. Should something interfere with cloud synchronization, the app still has a working location and continues tofunction normally.
Finally, consider the -restoreAnnotation method you wrote earlier. It never considered the possibility that there was an existing map annotation. That’s because the only place it was sent was when your
app started. Now, it can be received at any time, to either set or clear the saved map location. Add an else clause to the end of the method to take care of that possibility (new code in bold):
if (restoreInfo!=nil)
{
MKPointAnnotation *restoreAnnotation = [MKPointAnnotation new]; [restoreAnnotation restoreState:restoreInfo];
[self setAnnotation:restoreAnnotation];
}
else
{
[self setAnnotation:nil];
}
Enabling iCloud
All of your iCloud code is ready to run, but there’s just one problem: none of it will work. Before an app can use the iCloud servers, you must add an iCloud entitlement to your app. This, in turn, requires that
you register your app’s bundle identifier with Apple and obtain an entitlement certificate. These aren’t
complicated steps, but they are required.
Select the Pigeon project in the navigator. Make sure the Pigeon target is selected (either from the sidebar or the pop-up menu) and switch to the Capabilities tab. Locate the iCloud section and turn it on, as shown in Figure 18-3.
Figure 18-3. Enabling iCloud services
Choose the developer team that will be testing this app and click Choose. Xcode will register your app’s unique ID with the iOS Dev Center and enable that ID for use with the iCloud service. It will then download and install thenecessary entitlement certificates that permit your app to use the iCloud servers. You should now enable use of the key-value store, as shown in Figure 18-4.
This is the iCloud service that the NSUbiquitousKeyValueStore class depends on.
Figure 18-4. Enabling iCloud’s key-value store
When you enabled the key-value store, Xcode generates one ubiquity container identifier. This identifier is used to collate and synchronize all of the values you put in NSUbiquitousKeyValueStore. Normally, you use the
bundleidentifier of your app which is the default. This keeps your app’s iCloud values separate from the iCloud values stored by any of the user’s other apps.
Testing the Cloud
To test the cloud version of Pigeon, you’ll need two, provisioned, iOS devices. Both devices will need active Internet connections, be logged into the same iCloud account, and have iCloud Documents & Data turned on.
Start the Pigeon app running on both devices. Tap the “remember location” button on one device, give it a name, and wait. If everything
was set up properly, an identical pin should appear on the other device, typically within a minute.
Try remembering a location on the second device. Try clearing the location
You don’t need to have both apps running simultaneously—that’s just the coolest way to experience iCloud syncing. Launch Pigeon on one device, remember a location, and quit it. Count to twenty. Launch Pigeon on a seconddevice, and you’ll instantly see the updated location. That’s because
the ubiquitous key-value store works constantly in the background, whenever it has an Internet connection, to keep all of your values in sync.
Not everyone will want their map locations shared with all of their other devices. Some users would be perfectly happy with the first, non-cloud, version of Pigeon. Why not make all of your users happy and give them the option?
Add a configuration setting so they can opt-in to cloud synchronization, or leave it off. The question now is where do you put that setting? Do you add it to the map options view controller? Do you create another settings buttonthat takes the user to a second settings view? Maybe you’d add a tiny button with a little cloud icon to the map view? That would be pretty cute.
There are lots of possibilities, but I want you to think outside the box. Or, more precisely, I want you to think outside your app. Your task is to create an interface to let the user turn cloud synchronization on or off, but don’t put it in your app. Confused? Don’t be; it’s easier than you think.
Bundle Up Your Settings
A settings bundle is a property list file describing one or more user default values that your users can set. See, yet another use for property lists. Users set them, not in your app, but in the Settings app that comes with everyiOS system. Using a settings bundle is quite simple:
You create a list of value descriptions.
iOS turns that list into an interface that appears in the Settings app.
The user launches the Settings app and makes changes to their settings.
The updated values appear in your app’s user defaults.
Settings bundles are particularly useful for settings the user isn’t likely to change often and you don’t want cluttering up your app’s interface. For Pigeon, you’re going to create a trivially simple settings bundle with one option:synchronize using iCloud. The possible values will be on or off (YES or NO). Let’s get started.
Creating a Settings Bundle
In the Pigeon project, choose the New ➤ File ... command (via the File menu or by right/control-clicking in the project navigator). In the iOS section, locate the Resource group and select the Settings Bundle template, as shown in Figure 18-5.
Figure 18-5. Creating a settings bundle resource
Make sure the Pigeon target is selected, and add the new Settings resource to your project.
A settings bundle contains one property list file named Root.plist. This file contains a dictionary. You can see
this in Figure 18-6. The Root.plist file describes the settings that appear (first) when the user selects your app in theSettings app.
Figure 18-6. Property list from the settings bundle template
The dictionary contains an array value for the key Preference Items. That array contains a list of dictionaries. Each dictionary describes one setting or organization item. The kinds of setting you can include are listed in Table 18-2and the organizational items are in Table 18-3. The details for each type are described in the “Implementing an iOS Settings Bundle” chapter of the Preferences and
Settings Programming Guide that you can find in Xcode’s Documentation and API Reference window.
Table 18-2. Settings bundle value types
| |||
Settings Type
|
Key
|
Interface
|
Value
|
Text Field
|
PSTextFieldSpecifier
|
Text field
|
A string
|
Toggle Switch
|
PSToggleSwitchSpecifier
|
Toggle switch
|
Any two values, but YES and NO are thenorm
|
Slider
|
PSSliderSpecifier
|
Slider
|
Any number within a range
|
Multi-value
|
PSMultiValueSpecifier
|
Table
|
One value in a list of values
|
Radio Group
|
PSRadioGroupSpecifier
|
Picker
|
One value in a list of values
|
Title
|
PSTitleValueSpecifier
|
Label
|
Display only (value can’t be changed)
|
Table 18-3. Settings bundle organization types
| |||
Settings Type
|
Key
|
Description
| |
Group
|
PSGroupSpecifier
|
Organizes the settings that follow into a group.
| |
Child Table
|
PSChildPaneSpecifier
|
Presents a table item that, when tapped, presents another setof settings, creating a hierarchy of settings.
| |
Your settings bundle can invite the user to type in a string (like a nickname), let them turn settings on and off, pick from a list of values (“map,” “satellite,” “hybrid”), or choose a number with a slider. If your app has a lot ofsettings, you can organize them into groups or even link to another set with even more settings.
The values shown in Figure 18-6 present three settings in a single group named, rather unimaginatively, Group. Those settings consist of a text field, a toggle switch, and a slider.
For Pigeon, you only have one Boolean setting. Select the Root.plist file and used Xcode’s property list editor to make the following changes:
1. Select the row Item 3 (Slider) and press the delete key (or choose Edit ➤ Delete).
2. Select the row Item 1 (Text Field - Name) and press the delete key (or choose Edit ➤ Delete).
3. Expand the row Item 0 (Group - Group).
a. Change the value of its Title to iCloud
4. Expand the row Item 1 (Toggle Switch - Enabled)
a. Change the Default Value to NO
b. Change the Identifier to HPSyncLocations
c. Change the Title to Sync Locations
Your finished settings bundle should look like the one in Figure 18-7.
Figure 18-7. Pigeon settings bundle
Using Your Settings Bundle Values
Your settings bundle is complete. All that’s left is to put the values you just defined to work in your app. Select the HPViewController.h file and add this constant:
#define kPreferenceLocationsInCloud @"HPSyncLocations"
Switch to your HPViewController.m file, locate the -viewDidLoad method, and add the following conditional to your cloud store setup code (new code in bold):
if ([userDefaults boolForKey:kPreferenceLocationsInCloud])
{
cloudStore = [NSUbiquitousKeyValueStore defaultStore]; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self
selector:@selector(cloudStoreChanged:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object:cloudStore]; [cloudStore synchronize];
}
That’s it! If you’re saying “but what about all of those places in the code that store values into cloudStore,” you don’t have to worry about those. Your existing code takes advantage of an Objective-C feature that ignores messages sent to nil objects. If the kPreferencesLocationsInCloud value is NO, cloudStore never gets set and remains nil. Messages sent to nil, like [cloudStore removeObjectFor Key:kPreferenceSavedLocation], do nothing. The net effect is that, withcloudStore set to nil, Pigeon doesn’t make any changes to iCloud’s ubiquitous key-value store, and it won’t receive any notifications of changes. For a complete explanation, see the “nil is
Your Friend” section in Chapter 20.
Testing Your Settings Bundle
Run Pigeon, as shown in Figure 18-8.
If you still have two iOS devices connected, you can verify that your app is no longer saving the map location to the cloud. Each app is functioning independently of the other.
Figure 18-8. Testing the settings bundle
In Xcode, stop your app(s). This will return you to the springboard (second screen shot in Figure 18-8).
Locate your Settings app and launch it. Scroll down until you find the Pigeon app (third screen shot in Figure 18-8). Tap it, and you’ll see the settings you defined (on the right in Figure 18-8).
Change your Sync Locations setting to on—do this in both devices—and run your apps again. This time, Pigeon uses iCloud synchronization to share the map location.
Summary
Pigeon can no longer be accused of being a bird brained app! Not only will it remember the location the user saved, but also the map style and tracking mode they last set. In doing this, you learned how to store
property listvalues into the user defaults, how to convert non- property list objects into ones suitable to store, and how to get them back out again. More importantly, you understand the best times to store and retrieve those values.
You learned how to handle the situation where a user defaults value is missing, and how to create and register a set of default values. You also used user defaults to preserve the view controller states, whichgives your app asense of persistence. You did this by leveraging the powerful view controller restoration
facility, built into iOS.
You also took flight into the clouds, sharing and synchronizing changes using the iCloud storage service.
iCloud integration adds a compelling dimension to your app that anyone with more than one iOS device will appreciate.And if that wasn’t enough, you defined settings the user can access outside of your app.
You’ve taken another important step in creating apps that act the way users expect. But it was a tiny step. User defaults, and particularly the ubiquitous key-value store, are only suitable for small amounts of information. Tolearn how to store “big data,” step into the next chapter.







No comments:
Post a Comment