Wednesday, 7 May 2014

Assigning Restoration Identifiers [Remember Me?]

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 controllers 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 its simplest to set them there. Select the Main_iPhone.storyboard (or _iPad) file. Select the root tab bar view controllerand switch to the identity inspectoras 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, wont 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 
characters 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 wouldnt 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. Thats 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 its 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 numberIf it does, it restores the page number before the view appears, returning the user to where they were when they quit. That wasnt 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 users position in the table.

n     You can encode and restore anything you want in your app delegates
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 Xcodes 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 iPads map view change too. On the other hand, if your user was reading Alices Adventures in Wonderland ontheir iPad, wouldnt 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 doesnt spell out exactly what reasonable” is, but its 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 arent 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 its 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 doesnt 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 cant be updated just now, that wont 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 thats 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 theres only one value, so you dont 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. Thats 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 theres 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 apps bundle identifier with Apple and obtain an entitlement certificate. These arent 
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 apps 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 iClouds 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 appiCloud values separate from the iCloud values stored by any of the users 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 dont need to have both apps running simultaneously—thats 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. Thats 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 tthink outside your app. Your task is to create an interfacto let the user turn cloud synchronization on or off, but dont put it in your app. Confused? Dont be; its 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 apps user defaults.

Settings bundles are particularly useful for settings the user isnt likely to change often and you dont want cluttering up your apps 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). Lets 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 Resourc 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 dictionaryYou 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 odictionaries. 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 Xcodes 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 cant 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 unimaginativelyGroup. 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 Xcodes property list editor to make the following changes:

1.       Select the row Ite 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 thats 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];
}

Thats it! If you’re saying “but what abouall of those places in the code thastore values intcloudStore,” you dont 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 NOcloudStore never getset and remains nil. Messages sent to nil, like [cloudStore removeObjectFor Key:kPreferenceSavedLocation], do nothing. The net effect is that, withcloudStore set to nil, Pigeon doesnt make any changes to iClouds ubiquitous key-value store, and it wont 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 youapis no longer saving the map location tthe cloud. Each apis 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 wasnt 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