Remember Me - Learn iOS 8 App Development, Second Edition (2014)

Learn iOS 8 App Development, Second Edition (2014)

Chapter 18. Where Are You?

One of the marvelous qualities of iOS devices, which make them such an indispensable part of our lives, is their ability to remember so much stuff: pictures, phone numbers, addresses, appointments, to-do lists, lesson notes, project ideas, keynote presentations, playlists, articles you want to read—the list seems endless. But so far, none of the apps you’ve developed in this book remembers anything. MyStuff starts with an empty list every time you launch it. Wonderland doesn’t even remember what page you were reading. And consider Pigeon, poor Pigeon. Its only task is to memorize one location, and it can’t even do that. You’re going to fix all of that, and more.

As you might imagine, there are lots of different ways of storing information in iOS. The next two chapters will explore the basic ones. You’re going to begin with user defaults (sometimes called preferences). This is the technology most often used to remember small bits of information such as your settings, what tab you were viewing, what page number you were last looking at, your list of favorite URLs, and so on. In this chapter, you will do the following:

· Learn about property lists

· Add and retrieve values from the user defaults

· Create a settings bundle for your app

· Store and synchronize property list data in the cloud

· Preserve and restore views and view controllers

The mechanics of property lists and how to use them are simple; it will only take a page or two to explain the whole thing. How best to use them is another matter. Much of this chapter will be focused on the strategies of using property lists, so put on your thinking cap and let’s get started.

Property Lists

A property list is a graph of objects, where every object is one the following classes:

· NSDictionary

· NSArray

· NSString

· NSNumber (any integer, floating point, or Boolean value)

· NSDate

· NSData

While a property list can be a single string, it is most often a dictionary that contain strings, numbers, dates, or other arrays and dictionaries. Instances of these classes are called property list objects.

Seriously, that’s it.

Serializing Property Lists

Property lists are used throughout iOS because they are flexible, universal, and easily serialized. In this case, serialize (the Cocoa term) means “serialize” (the computer science term). Cocoa uses the term serialization to mean converting a property list into a transportable stream of bytes. You don’t often serialize property lists yourself, but they are regularly serialized behind the scenes.

Note A property list can be serialized into two different formats: binary and XML. The binary format is unique to Cocoa. It can be read and understood only by another Cocoa (OS X) or Cocoa Touch (iOS) app. The XML format is universal and can be exchanged with practically any computer system in the world. The advantage of the binary format is efficiency (both size and speed). The advantage of the XML format is portability.

A serialized property list written to a file is called a property list file, often a .plist file. Xcode includes a property list editor so you can directly create and modify the contents of a property list file. You’ll use the property list editor later in this chapter.

For the Wonderland app, I wrote a Mac (OS X) utility application that generated the Characters.nsarray resource file. That was a property list (an array of dictionaries containing strings), serialized in the XML format, and written to a property list file. Later, you added that as a resource file, which your app turned back into an NSArray object by deserializing the file.

Tip If you want to serialize a property list yourself, use the NSPropertyListSerialization class or one of the writeTo(...) methods in NSArray and NSDictionary.

User Defaults

One of the premier uses of property list objects is in the user defaults. The user defaults is a dictionary of property list objects you can use to store small amounts of persistent information, such as preferences and display state. You can store any property list value you want into the user defaults (NSUserDefaults) object and later retrieve it. The values you store there are serialized and preserved between runs of your app.

A user defaults (NSUserDefaults) object is created when your app starts. Any values you stored there the last time are deserialized and become immediately available. If you make any changes to the user defaults, they are automatically serialized and saved so they’ll be available the next time your app runs.

Note The user default values are local to your app. In other words, your app can’t get or change the values stored by other iOS apps.

Using NSUserDefaults is really simple. You obtain your app’s singleton user defaults object using the NSUserDefaults.standardUserDefaults() function. You call “set” function to store values (setInteger(_:,forKey:), setObject(_:,forKey:),setBool(_:,forKey:), and so on). You retrieve values by calling the “get” functions (integerForKey(_:), objectForKey(_:), boolForKey(_:), and so on).

Making Pigeon Remember

You’re going to use user defaults to give Pigeon some long-term memory. When you add user defaults to an app, you need to consider the following:

· What values to store

· What property list objects and keys you will use

· When to store the values

· When to retrieve the values

Each decision affects subsequent ones, so start at the top. For Pigeon, you want it to remember the following:

· The remembered map location (duh)

· The map type (plain, satellite, or hybrid)

· The tracking mode (none or follow heading)

The next step is to decide what property list objects you’re going to use to represent these properties. The map type and tracking mode are easy; they’re both integer properties, and you can store any integer value directly in the user defaults.

The MKPointAnnotation object that encapsulates the map location, however, isn’t a property list object and can’t be stored directly in the user defaults. Instead, its significant properties need to be converted into property list objects, which can be stored. The typical technique is to turn your information into either a string or a dictionary of property list objects, both of which are compatible with user defaults. For Pigeon, you’re going to convert the annotation into a dictionary containing three values: its latitude, its longitude, and its title. This is enough information to reconstruct the annotation when the app runs again.

You also have to pick keys to identify each value stored. At the top level, you want to choose keys that won’t be confused with any keys iOS might be using. A number of iOS frameworks also use your app’s user defaults to preserve information. The simplest technique is to use a prefix that isn’t used by iOS. Keys for iOS properties invariably use the two-letter prefix of the classes that store that value. (This is a relic of Objective-C class names.) For example, it’s unlikely the keys HPMapType and HPFollowHeading would conflict with any reserved iOS keys because there are no Cocoa Touch classes with an HP prefix. Keys used for values in subdictionaries can be anything you want.

Tip How do you know if a two-letter prefix is used by a Cocoa Touch class? Open the Documentation and API Reference window. Type in your two letters and see whether any class names appear in the search results.

Minimizing Updates and Code

With the first part out of the way, you can now turn your attention to the much subtler problem of deciding when and where to preserve your values in the user defaults and when to get them back out again.

Tackle the storage problem first. As a rule, you want to make updates to the user defaults as infrequently as practical while still keeping your code simple. The following are the common solutions:

· Capture the value when it changes.

· Capture the value at some dependable exit point.

The first solution is perfect for Pigeon. It saves only three values, and none of those values change that often. The user might change map type and heading from time to time, but they’re unlikely to fiddle with those settings a hundred times a minute. Likewise, the user will save a location when they arrive somewhere but won’t save another location until they’ve traveled someplace else.

The reason you want to limit user default updates is that every change triggers a chain of events that results in a fair amount of work occurring in the background. It’s something to avoid, as long as it doesn’t overly complicate your design. A good design will minimize updates with a minimal amount of code. When you start working with cloud-based storage (later in this chapter), it’s even more important to avoid gratuitous changes.

On the other hand, some values you want to preserve might change all the time or in many different places. For example, remembering the playback location of an audio book is something that changes constantly. It would be ludicrous to capture the playback position every second the audio was playing. Instead, it makes a lot more sense to simply note the user’s current playback position when they exit the app. You’ll explore that technique later in this chapter.

You’re going to start by preserving the map type and tracking mode because these are the simplest. Then you’ll tackle preserving and restoring the map location.

Defining Your Keys

This tutorial starts with the version of Pigeon in the exercise for Chapter 17. You’ll find that version in the Learn iOS Development Projects image Ch 17 image Pigeon E1 folder. If you came up with your own solution to the exercise, you should have no problem adapting this code to your app.

Begin by defining the keys used to identify values in your user defaults. Select the ViewController.swift file and add an enum with three constants.

enum PreferenceKey: String {
case MapType = "HPMapType"
case Heading = "HPFollowHeading"
case SavedLocation = "HPLocation"
}

Writing Values to User Defaults

Locate the code where the map type and tracking mode get changed. If you’re working with the version of Pigeon I wrote for Chapter 17, that code is in OptionsViewController.swift. Find the code where each setting gets changed. In OptionsViewController that happens in the changeMapStyle(_:_ and changeHeading(_:) functions. Change the code so it looks like the following (new code in bold):

@IBAction func changeMapStyle(sender: UISegmentedControl!) {
if let selectedMapType = MKMapType(rawValue:UInt(sender.selectedSegmentIndex)) {
if let mapView = (presentingViewController as? ViewController)?.mapView {
mapView.mapType = selectedMapType
let userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setInteger( Int(selectedMapType.rawValue),
forKey: PreferenceKey.MapType.rawValue)
}
}
}

@IBAction func changeHeading(sender: UISegmentedControl!) {
if let selectedTrackingMode = MKUserTrackingMode(rawValue:sender.selectedSegmentIndex+1) {
if let mapView = (presentingViewController as? ViewController)?.mapView {
mapView.userTrackingMode = selectedTrackingMode
let userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setInteger( selectedTrackingMode.rawValue,
forKey: PreferenceKey.Heading.rawValue)
}
}
}

The change is straightforward, and you should have no problem adapting the same idea to your own app. When a setting is changed, the new value is also stored in the user defaults. That’s all you have to do. NSUserDefaults takes care of everything else: converting the simple integer value into the appropriate property list (NSNumber) object, serializing the values, and storing them so they’ll be available the next time your app runs.

That’s the first half. Now you need to add the code to retrieve these saved values and restore the map options when your app starts.

Getting Values from User Defaults

Select the ViewController.swift file and locate the viewDidLoad() function. Replace the mapView.userTrackingMode = .Follow statement with the following code:

let userDefaults = NSUserDefaults.standardUserDefaults()
mapView.mapType = MKMapType(rawValue: UInt(userDefaults.integerForKey( image
PreferenceKey.MapType.rawValue)))!

if let trackingValue = userDefaults.objectForKey(PreferenceKey.Heading.rawValue) image
as? NSNumber {
mapView.userTrackingMode = MKUserTrackingMode(rawValue: trackingValue.integerValue)!
} else {
mapView.userTrackingMode = .Follow
}

This new code retrieves the integer values for the map type and tracking mode from the user defaults and uses them to restore those properties before the map is displayed. Now when the user runs the app and changes the map type, every time they launch the app after that, the map type will be the same.

But there’s a hitch. The first time the app is run—or if the user never changes the map type or tracking mode—there are no values at all for those keys in the user defaults. If you request the property list object for a nonexistent key, user defaults will return nil. If you request a scalar value (Boolean, integer, or floating-point), user defaults will return NO, 0, or 0.0. Here are three ways of dealing with this situation:

· Choose your values so that nil, false, 0, or 0.0 is the default

· Test to see whether user defaults contains a value for that key

· Register a default value for that key

The map type property adopts the first solution. Conveniently, the initial map type in Pigeon is .Standard, whose integer value is 0. So if there is no value in user defaults for the .MapType key, it returns a 0 and sets the map type to standard—which is perfect.

The tracking mode isn’t so lucky. The initial tracking mode Pigeon uses is .Follow, whose integer value is 1. If there’s no value for the .Heading key, you don’t want to set trackingMode to .None (0) by mistake.

Instead, the code uses the second solution. It first gets the property list (NSNumber) object for that key. If there’s no value for that key, user defaults returns nil and you know that a tracking value has never been set. You use this knowledge to either restore the user-selected mode or set the correct default.

Tip Use the method objectForKey(_:) to test for the presence of any value. A property list object ultimately represents every value in a property list. The objectForKey(_:) function returns an optional that you can use to test for nil.

That’s everything you need to preserve and restore these map settings. It’s time to test it, but that will require a little finesse.

Testing User Defaults

Using either a provisioned device or the simulator, run your updated Pigeon app. Tap the settings button and change the map type and tracking mode, as shown in Figure 18-1. This will update the user defaults with the new values, but those values may, or may not, be saved in persistent storage yet. That’s because the user defaults tries to be as efficient as possible and may wait for additional changes before beginning the serialization and storage process.

image

Figure 18-1. Testing the map settings

One way to get its attention is to push your app into the background. Do this by tapping the home button or use the Hardware image Home command in the simulator, shown in the third image in Figure 18-1. When your app enters the background, it doesn’t immediately stop running, but it prepares itself for that eventuality. One of those steps is to serialize and preserve all of your user defaults. Take a deep breath and count slowly to five.

With your user defaults safely stored, you can now stop your app and start it running again. Switch back to Xcode and click the stop button. Once the app stops, click the run button. The app starts up again. This time, it loads the map type and tracking mode from the saved user defaults and restores those properties. When the view controller loads, the map is exactly as the user left it last time.

Congratulations, you’ve learned the basics of preserving and restoring values in the user defaults. In the next few sections you’re going to refine your technique a little and deal with the (slightly) more complex problem of preserving and restoring the user’s saved map location.

Registering Default Values

The code to restore the tracking mode is awfully ugly. Well, maybe not awfully ugly, but it’s a little ugly. If you had a dozen of these settings to restore, you’d have a lot of repetitive code to write. Fortunately, there’s a more elegant solution.

Your app can register a set of default values for specific keys in user defaults—yes, they’re default defaults. When your code requests a value (userDefaults.integerForKey("Key")), the user defaults checks to see whether a value for that key has been previously set. If not, it returns a default value. For integers, that value is 0—unless you’ve specified something else. You do that using the registerDefaults(_:) method.

Select the AppDelegate.swift file. This is your app’s delegate object. It receives a lot of calls about the state of your app. One of those is the application(_:,willFinishLaunchingWithOptions:) function. This is the first call your app delegate object receives and is normally the first opportunity for code that you’ve written to run.

Near top of the file, add the following import so your new code can use the Map Kit constants. (You’ll find the finished version in the Learn iOS Development Projects image Ch 18 image Pigeon-2 folder.)

import MapKit

In your AppDelegate class, add the following function (or update it if one already exists):

func application(application: UIApplication, willFinishLaunchingWithOptions image
launchOptions: [NSObject : AnyObject]?) -> Bool {
let userDefaults = NSUserDefaults.standardUserDefaults()
let pigeonDefaults = [ PreferenceKey.Heading.rawValue: image
MKUserTrackingMode.Follow.rawValue ]
userDefaults.registerDefaults(pigeonDefaults)
return true
}

The registerDefaults(_:) function establishes a backup dictionary for the user default’s primary dictionary. The user defaults object actually manages several dictionaries, arranged into domains. When you ask it to retrieve a value, it searches each domain until it finds a value and returns it. The registerDefaults(_:) method sets up a domain behind all of the others, so if none of the other domains contains a value for PreferenceKey.Heading, this dictionary provides one.

Note Each domain in the user defaults has its own purpose and properties. The domain into which you store values is persistent; it will be serialized and preserved between app runs. The registration domain is not persistent. The values you pass to registerDefaults(_:)disappear when your app quits. You can read about domains in “The Organization of Preferences” chapter of the Preferences and Settings Programming Guide.

Now you can clean up the code in viewDidLoad(). Return to ViewController.swift and replace the code you previously added with this (updated code in bold):

let userDefaults = NSUserDefaults.standardUserDefaults()
mapView.mapType = MKMapType(rawValue: UInt(userDefaults.integerForKey( image
PreferenceKey.MapType.rawValue)))!
mapView.userTrackingMode = MKUserTrackingMode(rawValue: userDefaults.integerForKey( image
PreferenceKey.Heading.rawValue))!

Isn’t that a lot simpler? Because you’ve registered a defaults dictionary, your code doesn’t have to worry about the situation where there is no value for .Heading because now there will always be one.

Now that your map settings are persistent, it’s time to do something about that saved map location.

Turning Objects into Property List Objects

The big limitation of property lists is that they can contain only property list objects (NSNumber, NSString, NSDictionary, and so on). Anything you want to store in user defaults (or any property list) must be converted into one or more of those objects. Here are three most common techniques for storing other kinds of values:

· Convert the value into a string

· Convert the value into a dictionary containing other property list objects

· Serialize the value into an NSData object

The first technique is simple enough, especially since there are a number of Cocoa Touch functions that will do this for you. For example, let’s say you need to store a CGRect value in your user defaults. CGRect isn’t a property list object—it’s not even an object. You could store each of its four floating-point fields as separate values, like this:

let saveRect = someView.frame
let userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setFloat(Float(saveRect.origin.x), forKey: "HPFrame.x")
userDefaults.setFloat(Float(saveRect.origin.y), forKey: "HPFrame.y")
userDefaults.setFloat(Float(saveRect.height), forKey: "HPFrame.height")
userDefaults.setFloat(Float(saveRect.width), forKey: "HPFrame.width")

And you’d have to reverse the process to restore the rectangle. That seems like a lot of work. Fortunately, there are two functions—NSStringFromCGRect() and CGRectFromString()—that will convert a rectangle into a string object and back again. Now the code to save your rectangle can look something like this:

userDefaults.setObject(NSStringFromCGRect(saveRect), forKey: "HPFrame")

So if you can find functions that will convert your value to and from a property list object, use them.

The second technique is what you’re going to use for the map location. You’re going to write a pair of functions. The first will return the salient properties of your MKPointAnnotation object as a dictionary of NSString and NSNumber objects. A second method will take that dictionary and set them again.

Start by adding a new Swift file to your project. Drag a Swift File template from the template library (or choose the New image File... command) and drop it into your project. (You’ll find the finished version in the Learn iOS Development Projects image Ch 18 image Pigeon-3 folder.) Name the file PointAnnotationPreservation. Now write the following code in it:

import MapKit

enum LocationKey: String {
case Latitude = "lat"
case Longitude = "long"
case Title = "title'"
}

extension MKPointAnnotation {

var propertyState: [NSObject: AnyObject] {
get {
return [ LocationKey.Latitude.rawValue: NSNumber(double: coordinate.latitude),
LocationKey.Longitude.rawValue: NSNumber(double: coordinate.longitude),
LocationKey.Title.rawValue: title ]
}
set {
let lat = (newValue[LocationKey.Latitude.rawValue] as NSNumber).doubleValue
let long = (newValue[LocationKey.Longitude.rawValue] as NSNumber).doubleValue
coordinate = CLLocationCoordinate2D(latitude: lat, longitude: long)
title = newValue[LocationKey.Title.rawValue] as NSString
}
}
}

This code defines an extension to the MKPointAnnotation class. It adds a new (computed) property named propertyState. Getting the property returns a dictionary describing the location and title of the annotation. Setting the property updates the annotation’s location and title from the values in the dictionary.

Note An extension adds additional methods or properties to an existing class. You can use them to add new features to classes you didn’t write. I explain the ins and outs of extensions in Chapter 20.

This property allows you to get, and set, the relevant properties of an annotation in a form suitable for user defaults. Now let’s go use it to save and restore the map location.

Preserving and Restoring savedLocation

Return to ViewController.swift. You’re going to use the same technique you used to preserve and restore the map settings for the remembered map location. You’re going to save the location information (dictionary) when it’s established and restore it when the app starts again. ThesavedLocation object isn’t, however, a simple integer, so the code is a little more involved. Furthermore, you’re now establishing a new location from two places in the code: when the user sets it and when the app starts again. As you know by now, I’m not fond of repeating code, so I’m going to have you consolidate the code that sets the location. This will come in handy later, when you add a third avenue for setting the location.

To summarize, here’s what you’re going to change:

· Define a setAnnotation(_:) function to set or clear the saved location.

· Write preserveAnnotation() and restoreAnnotation() functions to store, and retrieve, the map location from the user defaults.

· Add code to saveAnnotation(_:) and clearAnnotation(_:) to preserve the map location.

· Restore any remembered location when your app launches.

Begin by adding the new setAnnotation(_:) function to the ViewController class:

func setAnnotation(annotation: MKPointAnnotation?) {
if savedAnnotation != annotation {
if let oldAnnotation = savedAnnotation {
mapView.removeAnnotation(oldAnnotation)
clearOverlay()
}
savedAnnotation = annotation
if annotation != nil {
mapView.addAnnotation(annotation)
mapView.selectAnnotation(annotation, animated: true)
}
}
}

This method will be used throughout ViewController to set, or clear, the annotation object. It follows a common setter method pattern that handles the cases where the savedAnnotation variable is nil, the annotation parameter is nil, both are nil, or neither is nil. It also deliberately takes no action if the same annotation object is set again.

The next step is to create functions that preserve and restore the annotation object using the user defaults. Add the following code to the ViewController class:

func preserveAnnotation() {
let userDefaults = NSUserDefaults.standardUserDefaults()
if let annotation = savedAnnotation {
userDefaults.setObject( annotation.propertyState, image
forKey: PreferenceKey.SavedLocation.rawValue)
} else {
userDefaults.removeObjectForKey(PreferenceKey.SavedLocation.rawValue)
}
}

func restoreAnnotation() {
let userDefaults = NSUserDefaults.standardUserDefaults()
if let state = userDefaults.dictionaryForKey(PreferenceKey.SavedLocation.rawValue) {
let restoreAnnotation = MKPointAnnotation()
restoreAnnotation.propertyState = state
setAnnotation(restoreAnnotation)
}
}

The first function converts the saved location into a property list dictionary using the propertyState getter you just wrote. It then stores that dictionary of values in the user defaults. If there is no saved location, it deliberately removes any previously stored values. The second function reverses the first, obtaining the preserved dictionary from the user defaults, using that to reconstruct an equivalent MKPointAnnotation object, and then sets it as the saved location.

Now you can alter your saveAnnotation(label:) and clearAnnotation() functions so they use the new setAnnotation(_:) and preserveAnnotation() functions (new code in bold).

func saveAnnotation(# label: String) {
if let location = mapView.userLocation?.location {
let annotation = MKPointAnnotation()
annotation.title = label
annotation.coordinate = location.coordinate
setAnnotation(annotation)
preserveAnnotation()
}
}

func clearAnnotation() {
setAnnotation(nil)
preserveAnnotation()
}

Note This is another example of refactoring. You’ve consolidated the work of maintaining the savedAnnotation variable in a new function but have preserved the behavior of the existing saveAnnotation(label:) and clearAnnotation() functions.

There’s only one thing left to do. In viewDidLoad(), add the following statement to the end of the function:

restoreAnnotation()

Pigeon now has the memory of an elephant! Reuse the test procedure you employed earlier to test the map settings:

1. Run Pigeon.

2. Remember a location on the map.

3. Press the home button to put the app in the background.

4. Stop the app in Xcode.

5. Run the app again.

When the app is restarted, the saved location is still there. Success!

This project demonstrates several common techniques for putting user defaults to work in your app. Remembering user preferences, settings, and working data (such as the saved map location) are all perfect uses for the user defaults.

Another common use is to save your app’s display state. When the user selects the Artists tab in the Music app and taps down into an album and ultimately a song, they aren’t surprised when they start Music the next day and find themselves at the same track, of the same album, of the same artist, in the Artists tab. That's because the Music app went to some effort to remember exactly what view controller the user left off at and reconstructed it the next time it was launched.

From what you know so far, you might think that you’d have to write code to capture the state of tab view and navigation view controllers, convert those into property list objects, store them in user defaults, and unroll the whole thing again when the app restarts. That’s basically what happens, but you’ll be happy to know that you don’t have to do (much of) that yourself. iOS has a specific mechanism for saving and restoring the state of your view controllers.

Persistent Views

In the section “Minimizing Updates and Code” I said the primary techniques for capturing user defaults were (a) when the value changes and (b) at a dependable exit point. You used technique (a) in Pigeon because it was a perfect fit. The values you were saving were only changed in a handful of places, and they change infrequently. But that isn’t always the case.

Some changes occur constantly (such as which view controller the user is in), and some changes occur in a myriad of different ways, making it difficult to catch them all. In these situations, the second approach is the best. You don’t worry about trying to monitor, or even care about, what changes are being made. Just arrange to capture that value before the user quits the app, dismisses the view controller, or exits whatever interface they’re using. There are two exit points that make good places to capture changes:

· Dismissing a view controller

· The app entering the background

For view controllers, you can capture your values in the code that dismisses the view controller. You might have to do a little extra work in circumstances such as a popover view controller because tapping outside the popover will dismiss it implicitly. You’d want to do something like override your view controller’s viewDidDisappear(_:) function so you don’t miss that exit route. But for the most part, it’s usually pretty easy to catch all of the ways a view controller can be dismissed.

Fading Into the Background

The other great place to capture changes, and particularly the view state, is when the app switches to the background. To appreciate this technique, you need to understand the states an iOS app progresses through. Your iOS app is always in one of these states:

· Not running

· Foreground

· Background

· Suspended

Your app is in the “not running” state before it’s launched, or after it’s ultimately terminated. Little happens when it’s not running.

The foreground state is the one you have the most experience with. This is when your app appears in the device’s display and your user is interacting with it. Foreground has two substates, active and inactive, that it jumps between. Active means your app is running. Inactive occurs when something interrupts it (such as a phone call or an alert), but it’s still being displayed. Your app’s code does not run when it’s inactive. The inactive state usually doesn’t last long.

Your app moves to the background state when you press the home button, switch to another app, or the screen locks. Your app continues to run for a short period of time but will quickly move to the suspended state.

Your app does not execute any code once suspended. If iOS later decides that it needs the memory your app is occupying or the user shuts down their device, your suspended app will terminate (without warning) and return to the not running state.

But your app might not be terminated. If the user relaunches your app or merely unlocks their screen and it’s still in the background state, your app isn’t restarted; it’s simply activated again. It moves directly to the foreground state and instantly resumes execution. Your app may enter and exit the background state repeatedly over its lifetime.

Note You can make special arrangements that allow your app to continue to run in the background. For example, you can request to play music or receive user location changes, even while your app is not the foreground app. See the section “Background Execution and Multitasking” in the iOS App Programming Guide for further details.

Apps take advantage of this small window of background processing to prepare themselves for termination. This is when the user defaults object serializes its property values and saves them to persistent storage. It’s also the perfect time to capture the state of your interface.

Your app can discover when it has entered the background state in two ways. Your app delegate object’s applicationDidEnterBackground(_:) function is called. Around the same time, a UIApplicationDidEnterBackgroundNotification notification is posted. Override that function or have any object observe that notification, and save whatever state information you need.

Caution iOS allots your app approximately five seconds of background processing time to save its state and finish up any work in progress. Your app must wrap up within that time or take explicit steps to enable background processing.

iOS also provides a mechanism to capture, and later restore, the state of your view controllers. This is automatically invoked when your app enters the background state.

Preserving View Controllers

As an example, take the Wonderland app. (I mean that, literally. Go find the finished Wonderland app from Chapter 12. You’re going to modify it.) The user can spend all day jumping between tabs, browsing characters in the table view, and flipping through the page view. You want to catch the point when the app switches to the background and remember what tab they had active and what page of the book they were looking at. You’ll use this to restore those views the next time the app is launched.

When an iOS app enters the background, iOS examines the active view controller. If properly configured, it will automatically preserve its state in the user defaults. This is a combination of what iOS already knows about the view controller and additional information that your code supplies. Specifically, iOS will remember what tab view was being displayed, the scroll position in a table view, and so on.

To that, you can add custom information that only your app understands. For Wonderland, you’re going to remember the page number the user was reading. (Remember that a page view controller has no concept of a page number; that’s something you invented for your page view controller data source.)

The first thing to address is the “properly configured” prerequisite. To put iOS to work for you, preserving and restoring your view controllers, you must do two steps:

1. Implement the application(_:,shouldSaveApplicationState:) and application(_:,shouldRestoreApplicationState:) app delegate functions.

2. Assign restoration identifiers to your view controllers, starting with the root view controller.

The first step tells iOS that you want its help in preserving and restoring your app’s view state. These functions must be implemented, and they must return true, or iOS will just pass your app by. They also serve a secondary function. If you have any custom, appwide state information that you want to preserve, these are the functions to do that in. Wonderland doesn’t have any, so it only needs to return true.

Open the Wonderland project from Chapter 12 and select the AppDelegate.swift file. Add the following two functions:

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

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

Assigning Restoration Identifiers

Once iOS is given the green light to save your view state, it 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 state information 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 preserved, 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 subview controller, capturing the state of each view controller with a restoration ID and ignoring those without.

Note The search for restorable view controllers skips any view controller that lacks a restoration ID. Thus, to save the state of a table view controller inside a navigation view controller inside a tab view controller, every one of those controllers must have a restoration ID, or else the state of the table view controller won’t be captured.

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.storyboard file in the Wonderland project. Select the root Tab Bar Controller in the Tab Bar Controller Scene and switch to the identity inspector, as shown in Figure 18-2. Locate the Restoration ID property and set it to RootTabBar.

image

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, however, won’t do you much good. What you want is the subview controller that was visible when the user quit Wonderland to reappear when they launch it again. For that to happen, each of the subview controllers must be restored too. Using the identity inspector, select each of the subview controllers and assign them restoration 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

RootTabBar

FirstViewController

Welcome

UINavigationController

CharacterNav

BookViewController

Book

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. Wait a moment.

5. Stop the app in Xcode.

6. Run the app again.

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 controller to 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 BookDataSource class. If you want to preserve and restore the page they were on, you’ll have to write some code to do that.

Each view and view controller object with a restoration ID receives an encodeRestorableStateWithCoder(_:) call when the app moves to the background. During application startup, it receives a decodeRestorableStateWithCoder(_:) message to restore itself. If you want to preserve custom state information, override these two functions.

Select the BookViewController.swift file. Add the following constant and two functions:

let pageStateKey = "pageNumber"

override func encodeRestorableStateWithCoder(coder: NSCoder) {
super.encodeRestorableStateWithCoder(coder)
let currentViewController = viewControllers[0] as OnePageViewController
coder.encodeInteger(currentViewController.pageNumber, forKey: pageStateKey)
}

override func decodeRestorableStateWithCoder(coder: NSCoder) {
super.decodeRestorableStateWithCoder(coder)
let page = coder.decodeIntegerForKey(pageStateKey)
if page != 0 {
let currentViewController = viewControllers[0] as OnePageViewController
currentViewController.pageNumber = page;
}
}

The first function obtains the current one page view controller being displayed in the page view controller. The OnePageViewController knows which page number it’s displaying. This number is saved in the NSCoder object.

Note NSCoder is the workhorse of iOS’s archiving framework. You use it by storing values and properties, which are converted into serialized data. You’ll learn all about NSCoder in the next chapter.

When your app is relaunched, the page view controller receives a decodeRestorableStateWithCoder(_:) call. It looks inside the NSCoder object to see whether it contains a saved (nonzero) 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 your new code. Launch Wonderland, flip through a few pages of the book, and 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:

· UIView objects can preserve their state too. Assign them a restoration ID and, if necessary, implement encodeRestorableStateWithCoder(_:) and decodeRestorableStateWithCoder(_:) functions. Remember that view controller that contains these views must have a Restoration ID for this to occur.

· 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 functions (indexPathForElementWithModelIdentifier(_:,inView:) and modelIdentifierForElementAtIndexPath(_:,inView:)) that remember and restore the user’s position in the table.

· You can encode and restore anything you want in your app delegate’s application(_:,shouldSaveApplicationState:) and application(_:,shouldRestoreApplicationState:) functions. You can use these methods to perform your own view controller restoration or use a combination of the automatic 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 of magic 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 you store 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 states are 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 were reading Alice’s Adventures in Wonderland on their 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. These limits are as follows:

· No more than 1MB of data, in total

· No more than 1,000 objects

· 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.

Caution If you abuse these limits, the iCloud servers may delay your updates or possibly stop synchronizing your data entirely.

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. You use 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 ViewController.swift and add a variable to hold a reference to the singleton cloud store object. (You’ll find the finished version in the Learn iOS Development Projects image Ch 18 image Pigeon-4 folder.)

var cloudStore: NSUbiquitousKeyValueStore?

Near the beginning of the viewDidLoad() function, add the following statement:

cloudStore = NSUbiquitousKeyValueStore.defaultStore()
cloudStore?.synchronize()

The first statement gets the cloud store object. The call to synchronize() prompts iOS to contact the cloud servers and update any values in the store that might have been changed by other iOS devices, and vice versa. This will happen eventually, but this hurries the process along when the app first starts and is the only time you’ll need to call synchronize().

Note There’s a reason I have you create and store a reference to the single NSUbiquitousKeyValueStore object, rather than just use NSUbiquitousKeyValueStore.defaultStore() when you need it. It will all make sense by the end of the chapter.

Now update your preserveAnnotation() function so it stores the annotation information in both the user defaults and the cloud (new code in bold).

func preserveAnnotation() {
let userDefaults = NSUserDefaults.standardUserDefaults()
if let annotation = savedAnnotation {
userDefaults.setObject( annotation.propertyState,
forKey: PreferenceKey.SavedLocation.rawValue)
cloudStore?.setDictionary( annotation.propertyState,
forKey: PreferenceKey.SavedLocation.rawValue)
} else {
userDefaults.removeObjectForKey(PreferenceKey.SavedLocation.rawValue)
cloudStore?.removeObjectForKey(PreferenceKey.SavedLocation.rawValue)
}
}

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 iOS device 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 should continue 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. If a value in the cloud should change, you should update your user defaults to match.

That 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 notification name in iOS). You’ll create a new function to process those changes, and you’ll need to register to receive them.

In your ViewController.swift file, find the viewDidLoad() function and augment the code that sets up the cloud store as follows (new code in bold):

cloudStore = NSUbiquitousKeyValueStore.defaultStore()
let center = NSNotificationCenter.defaultCenter()
center.addObserver( self,
selector: "cloudStoreChanged:",
name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification,
object: cloudStore)
cloudStore?.synchronize()

Caution You must register to observe change notifications before calling synchronize() or your app may miss preexisting changes in the cloud.

The cloudStoreChanged(_:) function will now be called whenever something in the cloud changes. The last step is to write that function.

func cloudStoreChanged(notification: NSNotification) {
let localStore = NSUserDefaults.standardUserDefaults()
if let cloudInfo = cloudStore?.dictionaryForKey(PreferenceKey.SavedLocation.rawValue) {
localStore.setObject(cloudInfo, forKey: PreferenceKey.SavedLocation.rawValue)
} else {
localStore.removeObjectForKey(PreferenceKey.SavedLocation.rawValue)
}
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 calls restoreAnnotation() to restore 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 in user defaults and continues to function normally.

Finally, consider the restoreAnnotation() function 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).

func restoreAnnotation() {
let userDefaults = NSUserDefaults.standardUserDefaults()
if let state = userDefaults.dictionaryForKey(PreferenceKey.SavedLocation.rawValue) {
let restoreAnnotation = MKPointAnnotation()
restoreAnnotation.propertyState = state
setAnnotation(restoreAnnotation)
} else {
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 from the pop-up menu) and switch to the General tab. Make sure your bundle identifier is a valid—that is, one that you own—reverse domain name that uniquely identifies your app (see “Launching Xcode for the First Time” in Chapter 1). This is the identifier you’ll register with Apple’s servers, and you can’t change it once registered.

Now switch to the Capabilities tab and locate the iCloud section, as shown in Figure 18-3. Turn it on.

image

Figure 18-3. Locating iCloud capability

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 the necessary entitlement certificates that permit your app to use the iCloud servers. You should now enable use of the key-value storage, as shown in Figure 18-4, if it isn’t checked already. This is the iCloud service that the NSUbiquitousKeyValueStore class depends on.

image

Figure 18-4. Enabling iCloud’s key-value store

When you enabled the key-value storage, Xcode generates one ubiquity container identifier, shown under Containers in Figure 18-4. This identifier is used to collate and synchronize all of the values you put in NSUbiquitousKeyValueStore. Normally, you use the bundle identifier 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.

Tip You’re allowed to share a key-value store identifier used by another app (that you wrote and registered). This allows your app to share a single key-value store with another app. You might do this, for example, if you’ve created a “lite” version and a “professional” version of your app. Both apps can use the same ­key-value store to share and synchronize their settings.

Testing the Cloud

To test the cloud version of Pigeon, you’ll need two provisioned iOS devices. The iOS simulator cannot access the ubiquitous cloud store. 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.

Tip Even if you have only one iOS device, you can still tell if NSUbiquitousKeyValueStore is working by checking the value returned by the call to synchronize(). If synchronize() returns true, then cloud values were successfully synchronized and everything is working. If it returns false, then there’s a problem. It could be network related. It could also mean your app’s identifier, entitlements, or provisioning profiles are not correctly configured.

You don’t need to have both apps running simultaneously—but that’s just the coolest way to experience iCloud syncing. Launch Pigeon on one device and remember a location. Launch Pigeon on a second device. Start counting, and probably before you get to 20, you’ll see the same location appear on the second device. Delete the location on the second device, and—in less than a minute—it will disappear on the first device. This is 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, noncloud 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 button that 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 every iOS system. Using a settings bundle is quite simple.

1. You create a list of value descriptions.

2. iOS turns that list into an interface that appears in the Settings app.

3. The user launches the Settings app and makes changes to their settings.

4. 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 (true or false). Let’s get started.

Creating a Settings Bundle

In the Pigeon project, choose the New image File command (via the File menu or by right-clicking or 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. (You’ll find the finished version of this project in the Learn iOS Development Projects image Ch 18 image Pigeon-5 folder.)

image

Figure 18-5. Creating a settings bundle resource

Make sure the Pigeon target is selected and add the new Settings resource to your project.

Caution Do not change the name of the new file. Your settings bundle must be named Settings.bundle, or iOS will ignore it.

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 the Settings app.

image

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-2, and 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

image

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 set of settings, creating a hierarchy of settings

Your settings bundle can invite the user to type in a string (such as 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 of settings, you can organize them into groups or even link to other screens 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 have only one Boolean setting. Select the Root.plist file and use Xcode’s property list editor to make the following changes. You’re going to discard the slider and text field (you don’t need them) and then repurpose the group and toggle switch.

1. Select the row Item 3 (Slider) and press the Delete key (or choose Edit image Delete).

2. Select the row Item 1 (Text Field - Name) and press the delete key (or choose Edit image Delete). Item 2 will now be named Item 1.

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.

image

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 ViewController.swift file and add one more enum case (new code in bold).

enum PreferenceKey: String {
case MapType = "HPMapType"
case Heading = "HPFollowHeading"
case SavedLocation = "HPLocation"
case CloudSync = "HPSyncLocations"
}

Locate the viewDidLoad() function, and add the following conditional around your cloud store setup code (new code in bold):

if userDefaults.boolForKey(PreferenceKey.CloudSync.rawValue) {
cloudStore = NSUbiquitousKeyValueStore.defaultStore()
let center = NSNotificationCenter.defaultCenter()
center.addObserver( self,
selector: "cloudStoreChanged:",
name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification,
object: cloudStore)
cloudStore?.synchronize()
}

Now your cloudStore object will only get set up and initialized if the user has set the .CloudSync toggle in the Settings app.

That’s it! If you’re asking “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 Swift optionals that can ignore functions called on nil objects. If the .CloudSync value is false,cloudStore never gets set and remains nil. When you suffix an object’s name with ? before a property or function call (as in cloudStore?.setDictionary(...)), Swift first checks to see whether cloudStore contains a reference. If it’s a valid object, it calls the function. But if it’s nil, it does nothing and skips to the next statement. The net effect is that, with cloudStore 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 “Optional Chaining” 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.

image

Figure 18-8. Testing the settings bundle

In Xcode, stop your app(s). This will return you to the springboard (the second image in Figure 18-8). Locate your Settings app and launch it. Scroll down until you find the Pigeon app (the third image in Figure 18-8). Tap it, and you’ll see the settings you defined (on the right in Figure 18-8).

Turn your Sync Locations setting 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 list values 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, which gives your app a sense 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. To learn how to store “big data,” step into the next chapter.

EXERCISE

You may have noticed a flaw in the last version of Pigeon—which I cleverly sidestepped by having you stop your app in Xcode before changing the Sync Location setting in the Settings app. Knowing what you now know about app states, the problem should be obvious.

Pigeon only examines the value of the .CloudSync value when it first starts. If a Pigeon user switches to the Settings app, changes the Sync Location setting, and then immediately returns to Pigeon, Pigeon is probably still running. It would have been moved the background state and suspended for a bit, but would be reactivated when the user returned. The bug is that Pigeon doesn’t check the value of .CloudSync again and won’t know that it has changed.

There are a couple of ways of solving this. One would be to add code to the applicationWillEnterForeground(_:) app delegate function. The solution I picked was to observe the NSUserDefaultsDidChangeNotification, posted by NSUserDefaults. Remember that the values in a settings bundle make changes to your app’s user defaults, and you can observe those changes via the notification center.

You’ll find my solution to this problem in the Learn iOS Development Projects image Ch 18 image Pigeon E1 folder. See if you can think of a third—very similar but more targeted—solution. (Hint, read the documentation for the applicationWillEnterForeground(_:)method.)