Chapter 12

Application Settings and User Defaults

All but the simplest computer programs today have a preferences window where the user can set application-specific options. On Mac OS X, the Preferences… menu item is usually found in the application menu. Selecting it brings up a window where the user can enter and change various options. The iPhone and other iOS devices have a dedicated application called Settings, which you no doubt have played with any number of times. In this chapter, we'll show you how to add settings for your application to the Settings application, and how to access those settings from within your application.

Getting to Know Your Settings Bundle

The Settings application lets the user enter and change preferences for any application that has a settings bundle. A settings bundle is a group of files built in to an application that tells the Settings application which preferences the application wishes to collect from the user.

Pick up your iOS device, and locate your Settings icon. By default, you'll find it on the home screen (see Figure 12–1).

images

Figure 12–1. The Settings application icon is in the middle of the last column on this iPhone. It may be in a different spot on your device, but it's always available.

When you touch the icon, the Settings application will launch. Ours is shown in Figure 12–2.

images

Figure 12–2. The Settings application

The Settings application acts as a common user interface for the iOS User Defaults mechanism. User Defaults is the part of the system that stores and retrieves preferences.

In an iOS application, User Defaults is implemented by the NSUserDefaults class. If you've done Cocoa programming on the Mac, you're probably already familiar with NSUserDefaults, because it is the same class that is used to store and read preferences on the Mac. Your applications will use NSUserDefaults to read and store preference data using a key value, just as you would access keyed data from an NSDictionary. The difference is that NSUserDefaults data is persisted to the file system, rather than stored in an object instance in memory.

In this chapter, we're going to create an application, add and configure a settings bundle, and then access and edit those preferences from within our application.

One nice thing about the Settings application is that it provides a solution so that you don't need to design your own user interface for your preferences. You create a property list defining your application's available settings, and the Settings application creates the interface for you.

Immersive applications, such as games, generally should provide their own preferences view so that the user doesn't need to quit in order to make a change. Even utility and productivity applications might, at times, have preferences that a user should be able to change without leaving the application. We'll also show you to how to collect preferences from the user directly in your application, and store those in iOS's User Defaults.

In addition, with the introduction of background processing in iOS 4, you can actually switch to the Settings application, change a preference, and then switch back to your application. We'll show you how to handle that situation at the end of this chapter.

The AppSettings Application

We're going to build a simple application in this chapter. First, we'll implement a settings bundle so that when the user launches the Settings application, there will be an entry for our application, AppSettings (see Figure 12–3).

images

Figure 12–3. The Settings application showing an entry for our AppSettings application in the simulator

If the user selects our application, Settings will drill down into a view that shows the preferences relevant to our application. As you can see from Figure 12–4, the Settings application uses text fields, secure text fields, switches, and sliders to coax values out of our intrepid user

images

Figure 12–4. Our application's primary settings view

Also notice the two items in the view that have disclosure indicators. The first one, Protocol, takes the user to another table view that displays the options available for that item. From that table view, the user can select a single value (see Figure 12–5).

The More Settings disclosure indicator allows the user to drill down to another set of preferences (see Figure 12–6). This child view can have the same kinds of controls as the main settings view, and can even have its own child views. You may have noticed that the Settings application uses a navigation controller, which it needs because it supports the building of hierarchical preference views.

images

Figure 12–5. Selecting a single preference item from a list

images

Figure 12–6. A child settings view for our application

When users launch our application, they will be presented with a list of the preferences gathered in the Settings application (see Figure 12–7).

images

Figure 12–7. Our application's main view

In order to show how to update preferences from within our application, we also provide a little information button in the lower-right corner. This button takes users to another view where they can change additional preferences directly in our application (see Figure 12–8).

images

Figure 12–8. Setting some preferences directly in our application

Let's get started building AppSettings, shall we?

Creating the Project

In Xcode, press imagesimagesN or select New images New images Project…. When the new project assistant comes up, select Application from under the iOS heading in the left pane, click the Utility Application icon, and click Next. On the next screen, name your project AppSettings. Set Device Family to iPhone. Next, check that the Use Storyboard and Use Automatic Reference Counting checkboxes are checked, and that the Use Core Data and Include Unit Tests checkboxes are unchecked, and then click the Next button. Finally, choose a location for your project and click Create.

We haven't used this particular project template before, so let's take a quick look at the project before we proceed. The Utililty Application template creates an application similar to the multiview application we built in Chapter 6. The application has a main view and a secondary view called the flipside view. Tapping the information button on the main view takes you to the flipside view, and tapping the Done button on the flipside view takes you back to the main view.

It takes several controllers and views to implement this type of application. All of these are provided, as stubs, by the template. Expand the AppSettings folder, where you'll find the usual application delegate class, as well as two additional controller classes and a storyboard file to contain the GUI (see Figure 12–9).

images

Figure 12–9. Our project created from the Utility Application template. Notice the application delegate, the storyboard, and the main and flipside view controllers.

Working with the Settings Bundle

The Settings application bases the display of preferences for a given application on the contents of the settings bundle inside that application. Each settings bundle must have a property list, called Root.plist, which defines the root-level preferences view. This property list must follow a very precise format, which we'll talk about when we set up the property list for our application.

When the Settings application starts up, it checks each application for a settings bundle and adds a settings group for each application that includes a settings bundle. If we want our preferences to include any subviews, we need to add property lists to the bundle and add an entry to Root.plist for each child view. You'll see exactly how to do that in this chapter.

Adding a Settings Bundle to Our Project

In theproject navigator, click the AppSettings folder, and then select File images New File… or press imagesN. In the left pane, select Resource under the iOS heading, and then select the Settings Bundle icon (see Figure 12–10). Click the Next button, leave the default name of Settings.bundle, and click Create.

images

Figure 12–10. Creating a settings bundle in Xcode

You should now see a new item in the project window, called Settings.bundle. If it's not already opened, expand Settings.bundle, and you should see two subitems: a folder named en.lproj, containing a file named Root.strings, andan icon named Root.plist. We'll discuss en.lproj in Chapter 21 when we talk about localizing your application into other languages. Here, we'll concentrate on Root.plist.

Setting Up the Property List

Select Root.plist, and take a look at the editor pane. You're looking at Xcode's property list editor (see Figure 12–11). This editor functions in the same way as the Property List Editor application in /Developer/Applications/Utilities.

images

Figure 12–11. Root.plist in the property list editor pane. If your editing pane looks slightly different, don't panic. Control-click in the editing pane and select Show Raw Keys/Values from the contextual menu that appears.

Notice the organization of the items in the property list. Property lists are essentially dictionaries, storing item types and values, and using a key to retrieve them, just as an NSDictionary does.

Several different types of nodes can be put into a property list. The Boolean, Data, Date, Number, and String node types are meant to hold individual pieces of data, but you also have a couple of ways to deal with whole collections of nodes as well. In addition to Dictionary node types, which allow you to store other nodes under a key, there are Array nodes, which store an ordered list of other nodes similar to an NSArray. The Dictionary and Array types are the only property list node types that can contain other nodes.

NOTE: Although you can use most kinds of objects as keys in an NSDictionary, keys in property list dictionary nodes must be strings. However, you are free to use any node type for the values.

When creating a settings property list, you need to follow a very specific format. Fortunately, Root.plist, the property list that came with the settings bundle you just added to your project, follows this format exactly. Let's take a look.

In the Root.plist editor pane, names of keys can either be displayed in their true, “raw” form or in a slightly more human-readable form. We're big fans of seeing things as they truly are whenever possible, so right-click anywhere in the editor and make sure the Show Raw Keys/Values option in the contextual menu is checked (see Figure 12–12). The rest of our discussion here uses the real names for all the keys we're going to talk about, so this step is important.

images

Figure 12–12. Control-click anywhere in the property list editing pane and make sure the Show Raw Keys/Values item is checked. This will ensure that real names are used in the property list editor, which makes your editing experience more precise.

CAUTION: As of this writing, leaving the property list, either by editing a different file or by quitting Xcode, resets the Show Raw Keys/Values item to be unchecked. If your text suddenly looks a little different, take another look at that menu item and make sure it is checked.

One of the items in the dictionary is StringsTable. A strings table is used in translating your application into another language. We'll cover the strings table in Chapter 21, when we get into localization. Since the strings table is optional, you can delete that entry by clicking it and pressing the delete key. Or you can leave it there if you prefer, since it won't do any harm.

In addition to StringsTable, the property list contains a node named PreferenceSpecifiers, which is an array. This array node is designed to hold a set of dictionary nodes, each representing a single preference that the user can enter or a single child view that the user can drill down into.

You'll notice that Xcode's template kindly gave us four nodes (see Figure 12–13). Those nodes aren't likely to reflect our actual preferences, so delete Item 1, Item 2, and Item 3 (select each one and press the delete key, one after another), leaving just Item 0 in place.

NOTE: To select an item in the property list, it is best to click on one side or another of the Key column, to avoid bringing up the Key column's drop-down menu.

images

Figure 12–13. Root.plist in the editor pane, this time with PreferenceSpecifiers expanded

Single-click Item 0 but don't expand it. Xcode's property list editor lets you add rows by pressing the return key. The current selection state—including which row is selected and whether or not it's expanded—determines where the new row will be inserted. When an unexpanded array or dictionary is selected, pressing return adds a sibling node after the selected row. In other words, it will add another node at the same level as the current selection. If you were to press return (but don't do that now), you would get a new row called Item 1 immediately after Item 0. Figure 12–14 shows an example of hitting return to create a new row. Notice the drop-down menu that allows you to specify the kind of preference specifier this item represents—more on this in a bit.

images

Figure 12–14. We selected Item 0 and hit return to create a new sibling row. Note the drop-down menu that appears, allowing us to specify the kind of preference specifier this item represents.

Now expand Item 0, and see what it contains (see Figure 12–15). The editor is now ready to add child nodes to the selected item. If you were to press return at this point (again, don't actually press itnow), you would get a new first child row inside Item 0.

images

Figure 12–15. When you expand Item 0, You'll find a row with a key of Type and a second row with a key of Title. This represents a group with a title of Group.

One of the items inside Item 0 has a key of Type. Every property list node in the PreferenceSpecifiers array must have an entry with this key. The Type key is typically the first entry, but order doesn't matter in a dictionary, so the Type key doesn't need to be first. The Type keytells the Settings application what type of data is associated with this item.

In Item 0, the Type item has a value of PSGroupSpecifier. This indicates that the item represents the start of a new group. Each item that follows will be part of this group, until the next item with a Type of PSGroupSpecifier.

If you look back at Figure 12–4, you'll see that the Settings application presents the application settings in a grouped table. Item 0 in the PreferenceSpecifiersarray in a settings bundle property list should always be a PSGroupSpecifierso the settings start in a new group, because you need at least one group in every Settings table.

The only other entry in Item 0 has a key of Title, and this is used to set an optional header just above the group that is being started.

Now, take a closer look at the Item 0 row itself, and you'll see that it's actually shown as Item 0 (Group – Group). The values in parentheses represent the value of the Type item (the first Group) and the Title item (the second Group). This is a nice shortcut that Xcode gives you so that you can visually scan the contents of a settings bundle.

As shown back in Figure 12–4, we've called our first group General Info. Double-click the value next to Title, and change it from Group to General Info (see Figure 12–16). When you enter the new title, you may notice a slight change to Item 0. It's now shown as Item 0 (Group – General Info) to reflect the new title.

images

Figure 12–16. We changed the title of the Item 0 group from Group to General Info.

Adding a Text Field Setting

We now need to add a second item in this array, which will represent the first actual preference field. We're going to start with a simple text field.

If you single-click the PreferenceSpecifiersrow in the editor pane (don't do this, just keep reading), and press return to add a child, the new row will be inserted at the beginning of the list, which is not what we want. We want to add a row at the end of the array.

To add the row, click the disclosure triangle to the left of Item 0 to close it, and then select Item 0 and press return, which will give you a new sibling row after the current row (see Figure 12–17). As usual, when the item is added, a drop-down menu appears, showing the default value of Text Field.

images

Figure 12–17. Adding a new sibling row to Item 0

Click somewhere outside the drop-down menu to make it go away, and then click the disclosure triangle next to Item 1 to expand it. You'll see that it contains a Type row set to PSTextFieldSpecifier. This is the Type value used to tell the Settings application that we want the user to edit this setting in a text field. It also contains two empty rows for Title and Key (see Figure 12–18).

images

Figure 12–18. Our text field item, expanded to show the type, title, and key

Select the Title row, then double-click in the whitespace of the Value column. Type in Username to set the Title value. This is the text that will appear in the Settings app.

Now do the same for the Key row(no, that's not a misprint, you're really looking at a key called Key). For a value, type in username (note the lowercase first letter). Remember thatuser defaults work like a dictionary. This entry tells the Settings application which key to use when it stores the value entered in this text field.

Recall what we said about NSUserDefaults? It lets you store values using a key, similar to an NSDictionary. Well, the Settings application will do the same thing for each of the preferences it saves on your behalf. If you give it a key value of foo, then later in your application, you can request the value for foo, and it will give you the value the user entered for that preference. We will use this same key value later to retrieve this setting from the user defaults in our application.

NOTE: Notice that our Title has a value of Username and our Key has a value of username. This uppercase/lowercase difference will happen frequently. The Title is what appears on the screen, so the capital U makes sense. The Key is a text string we'll use to retrieve preferences from the user defaults, so all lowercase makes sense there. Could we use all lowercase for Title? You bet. Could we use all capitals for Key? Sure! As long as you capitalize it the same way when you save and when you retrieve, it doesn't matter which convention you use for your preference keys.

Now, select the last of the three Item 1 rows (the one with a Key of Key) and press return to add another entry to our Item 1 dictionary, giving this one a key of AutocapitalizationType and a value of None. This specifies that the text field shouldn't attempt to autocapitalize what the user types in this field. Note that as soon as you start typing AutocapitalizationType, Xcode presents you with a list of matching choices, so you can simply pick one from the list instead of typing the whole name.

Create one last new row, and give it a key of AutocorrectionType and a value of No. This will tell the Settings application not to try to autocorrect values entered into this text field. When you do want the text field to use autocorrection, change the value in this row to Yes. Again, Xcode presents you with a list of matching choices as you begin entering AutocorrectionType.

When you're finished, your property list should look like the one shown in Figure 12–19.

images

Figure 12–19. The finished text field specified in Root.plist

Adding an Application Icon

Before we try out our new setting, let's add an application icon to the project. You've done this before.

Save Root.plist, the property file you just edited. Then make your way into the source code archive and into the 12 – AppSettings folder. Drag the file icon.png into your project's AppSettings folder and, when prompted, have Xcode copy the icon.

Next, open the Supporting Files folder, and click the file AppSettings-info.plist. When the property list editor appears, expand the Icon files row. Next, select the Icon files row, press return to create a new item inside it, and change the new item's value to icon.png.

That's it. Now compile and run the application by selecting Product images Run. Press the home button, and then tap the icon for the Settings application. You will find an entry for our application, which uses the application icon we added earlier (see Figure 12–3). Click the AppSettings row, and you will be presented with a simple settings view with a single text field, as shown in Figure 12–20.

images

Figure 12–20. Our root view in the Settings application after adding a group and a text field

Quit the simulator, and go back to Xcode. We're not finished yet, but you should now have a sense of how easy it is to add preferences to your application. Let's add the rest of the fields for our root settings view. The first one we'll add is a secure text field for the user's password.

Adding a Secure Text Field Setting

Click Root.plist to return to your setting specifiers (don't forget to turn on Show Raw Keys/Values, assuming your friends at XcodeCorp have reset this). Collapse Item 0 and Item 1. Now select Item 1. Press imagesC to copy it to the clipboard, and then press imagesV to paste it back. This will create a new Item 2 that is identical to Item 1. Expand the new item, and change the Title to Password and the Key to password (one with a capital P and one with a lowercase p).

Next, add one more child to the new item. Remember that the order of items does not matter, so feel free to place it directly below the Key item you just edited. To do this, select the Key/password row, and then hit return.

Give the new item a Key of IsSecure (note the leading uppercase I), and change the Type to Boolean. Now change its Value from NO to YES, which tells the Settings application that this field needs to be a password field, rather than just an ordinary text field. Our finished Item 2 is shown in Figure 12–21.

images

Figure 12–21. Our finished Item 2, a text field designed to accept a password

Adding a Multivalue Field

The next item we're going to add is a multivalue field. This type of field will automatically generate a row with a disclosure indicator. Clicking it will take you down to another table where you can select one of several rows.

Collapse Item 2, select the row, and then press return to add Item 3. Use the popup attached to the Key field to select Multi Value, and expand Item 3 by clicking the disclosure triangle.

The expanded Item 3 already contains a few rows. One of them, the Type row, is set to PSMultiValueSpecifier. Look for the Title row and set its value to Protocol. Then find the Key row, and give it a value of protocol. The next part is a little tricky, so let's talk about it before we do it.

We're going to add two more children to Item 3, but they will be Array type nodes, not String type nodes, as follows:

  • One array, called Titles, will hold a list of the values from which the user can select.
  • The other array, called Values, will hold a list of the values that actually are stored in the user defaults.

So, if the user selects the first item in the list, which corresponds to the first item in the Titles array, the Settings application will actually store the first value from the Values array. This pairing of Titles and Values lets you present user-friendly text to the user but actually store something else, like a number, date, or different string.

Both of these arrays are required. If you want them both to be the same, you can create one array, copy it, paste it back in, and change the key so that you have two arrays with the same content but stored under different keys. We'll actually do just that.

Select Item 3 (leave it open) and press return to add a new child. You'll see that once again, Xcode is aware of the type of file we're editing and seems to anticipate what we want to do, because the new child row already has its Key set to Titles and is configured to be an Array. Just what we wanted! Expand the Titles row and hit return to add a child node. Repeat this four more times, so you have a total of five child nodes. All five nodes should be String type and should contain the following values: HTTP, SMTP, NNTP, IMAP, and POP3.

Once you've entered all five nodes, collapse Titles, and select it. Then press imagesC to copy it, and press imagesV to paste it back. This will create a new item with a key of Titles - 2. Double-click Titles - 2, and change it to Values.

We're almost finished with our multivalue field. There's just one more required value in the dictionary, which is the default value. Multivalue fields must have one—and only one—row selected. So, we need to specify the default value to be used if none has yet been selected, and it needs to correspond to one of the items in the Values array (not the Titles array, if they are different). Xcode already added a DefaultValuerow when we created this item, so all we need to do now is give it a value of SMTP. Figure 12–22 shows our version of Item 3.

images

Figure 12–22. Our finished Item 3, a multivalue field designed to let the user select from one of five possible values

Let's check our work. Save the property list, and build and run the application again. When your application starts up, press the home button and launch the Settings application. When you select AppSettings, you should see three fields on your root-level view (see Figure 12–23). Go ahead and play with your creation, and then let's move on.

images

Figure 12–23. Three fields down. Not too shabby!

Adding a Toggle Switch Setting

The next item we need to get from the user is a Boolean value that indicates whether our warp engines are turned on. To capture a Boolean value in our preferences, we are going to tell the Settings application to use a UISwitch by adding another item to our PreferenceSpecifiersarray with a type of PSToggleSwitchSpecifier.

Collapse Item 3 if it's currently expanded, and then single-click it to select it. Press return to create Item 4. Use the drop-down menu to select Toggle Switch, and then click the disclosure triangle to expand Item 4. You'll see there's already a child row with a Key of Type and a Value of PSToggleSwitchSpecifier. Give the empty Title row a value of Warp Drive, and set the value of the Key row to warp.

We have one more required item in this dictionary, which is the default value. Just as with the Multi Value setup, here Xcode has already created a DefaultValue row for us. Let's turn on our warp engines by default by giving the DefaultValue row a value of YES. Figure 12–24 shows our completed Item 4.

images

Figure 12–24. Our finished Item 4, a toggle switch to turn the warp engines on and off. Engage!

Adding the Slider Setting

The next item we need to implement is a slider. In the Settings application, a slider can have a small image at each end, but it can't have a label. Let's put the slider in its own group with a header so that the user will know what the slider does.

Start by collapsing Item 4. Now, single-click Item 4 and press return to create a new row. Use the popup to turn the new item into a Group, and then click the item's disclosure triangle to expand it. You'll see that Type is already set to PSGroupSpecifier. This will tell the Settings application to start a new group at this location. Double-click the value in the row labeled Title, and change the value to Warp Factor.

Collapse Item 5 and select it, and then press return to add a new sibling row. Use the popup to change the new item into a Slider, which indicates to the Settings application that it should use a UISlider to get this information from the user. Expand Item 6 and set the value of the Key row to warpFactorso that the Settings application knows which key to use when storing this value.

We're going to allow the user to enter a value from 1 to 10, and we'll set the default to warp5. Sliders need to have a minimum value, a maximum value, and a starting (or default) value, and all of these need to be stored as numbers, not strings, in your property list. Fortunately, Xcode has already created rows for all these values. Give the DefaultValuerow a value of 5, the MinimumValue row a value of 1, and the MaximumValue row a value of 10.

If you want to test the slider, go ahead, but hurry back. We're going to do just a bit more customization.

As noted, sliders can have images. You can place a small 21 × 21-pixel image at each end of the slider. Let's provide little icons to indicate that moving the slider to the left slows us down, and moving it to the right speeds us up.

Adding Icons to the Settings Bundle

In the 12 -AppSettings folder in the project archive that accompanies this book, you'll find two icons called rabbit.png and turtle.png. We need to add both of these to our settings bundle. Because these images need to be used by the Settings application, we can't just put them in our AppSettings folder; we need to put them in the settings bundle so the Settings application can access them.

To do that, find the Settings.bundle in the project navigator. We'll need to open this bundle in the Finder. Control-click the Setting.bundle icon in the project navigator. When the contextual menu appears, select Show in Finder (see Figure 12–25) to show the bundle in the Finder.

images

Figure 12–25. The Settings.bundle contextual menu

Remember that bundles look like files in the Finder, but they are really folders. When the Finder window opens to show the Settings.bundle file, control-click the file and select Show Package Contents from the contextual menu that appears. This will open the settings bundle in a new Finder window, and you should see the same two items that you see in Settings.bundle in Xcode. Copy the two icon files, rabbit.png and turtle.png, from the 12 - AppSettings folder into the Settings.bundle package contents Finder window.

You can leave this window open in the Finder, as we'll need to copy another file here soon. Now, we'll return to Xcode and tell the slider to use these two images.

Back in Xcode, return to Root.plist and add two more child rows under Item 6. Give one a key of MinimumValueImage and a value of turtle.png. Give the other a key of MaximumValueImage and a value of rabbit.png. Our finished Item 6 is shown in Figure 12–26.

images

Figure 12–26. Our finished Item 6, a slider with turtle and rabbit icons to represent slow and fast

Save your property list, and let's build and run our app to make sure everything is still hunky-dory. You should be able to navigate to the Settings application and find the slider waiting for you, with the sleepy turtle and the happy rabbit at each end (see Figure 12–27).

images

Figure 12–27. We have text fields, multivalue fields, a toggle switch, and a slider. We're almost finished.

Adding a Child Settings View

We're going to add another preference specifier to tell the Settings application that we want it to display a child settings view. This specifier will present a row with a disclosure indicator that, when tapped, will take the user down to a whole new view full of preferences. Let's get to it.

Since we don't want this new preference to be grouped with the slider, first we'll copy the group specifier in Item 0 and paste it at the end of the PreferenceSpecifiers array to create a new group for our child settings view.

In Root.plist, collapse all open items, and then single-click Item 0 to select it and press imagesC to copy it to the clipboard. Next, select Item 6, and then press imagesV to paste in a new Item 7. Expand Item 7, and double-click the Value column next to the key Title, changing it from General Info to Additional Info.

Now, collapse Item 7 again. Select it, and press return to add Item 8, which will be our actual child view. Expand it by clicking the disclosure triangle. Find the Type row and give it a value of PSChildPaneSpecifier. Then set the value of the Title row to More Settings. You can ignore the Key row.

We need to add one final row to Item 8, which will tell the Settings application which property list to load for the More Settings view. Add another child row and give it a key of File and a value of More (see Figure 12–28). The file extension. plist is assumed and must not be included (if it is, the Settings application won't find the plist file).

images

Figure 12–28. Our finished Items 7 and 8, setting up the new Additional Info settings group and providing the child pane link to the file More.plist

We are adding a child view to our main preference view. The settings in that child view are specified in the More.plist file. We need to copy More.plist intothe settings bundle. We can't add new files to the bundle in Xcode, and the Property List Editor's Save dialog will not let us save into a bundle. So, we need to create a new property list, save it somewhere else, and then drag it into the Settings.bundle window using the Finder.

You've now seen all the different types of preference fields that you can use in a settings bundle plist file. To save yourself some typing, you can grab More.plist out of the 12 - AppSettings folder in the project archive that accompanies this book, and drag it into that Settings.bundlewindow we left open earlier.

TIP: When you create your own child settings views, the easiest way is to make a copy of Root.plist and give it a new name. Then delete all of the existing preference specifiers except the first one, and add whatever preference specifiers you need for that new file.

We're finished with our settings bundle. Feel free to compile, run, and test the Settings application. You should be able to reach the child view and set values for all the other fields. Go ahead and play with it, and make changes to the property list if you want.

TIP: We've covered almost every configuration option available (at least at the time of this writing). You can find the full documentation of the settings property list format in the document called Settings Application Schema Reference in the iOS Dev Center. You can get that document, along with a ton of other useful reference documents, from this page: http://developer.apple.com/library/ios/navigation/.

Before continuing, copy the rabbit.png and turtle.png icons from the 12 - AppSettings folder in the project archive into your project's AppSettings folder. We'll use them in our application to show the value of the current settings.

You might have noticed that the two icons you just added are exactly the same ones you added to your settings bundle earlier, and you might be wondering why. Remember that iOS applications can't read files out of other applications' sandboxes. The settings bundle doesn't become part of our application's sandbox; it becomes part of the Settings application's sandbox. Since we also want to use those icons in our application, we need to add them separately to our AppSettings folder so they are copied into our application's sandbox as well.

Reading Settings in Our Application

We've now solved half of our problem. The user can get to our preferences, but how do we get to them? As it turns out, that's the easy part.

Retrieving User Settings

We'll take advantage of a class called NSUserDefaults to read in the user's settings. NSUserDefaults is implemented as a singleton, which means there is only one instance of NSUserDefaults running in your application. To get access to that one instance, we call the class method standardUserDefaults, like so:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

Once we have a pointer to the standard user defaults, we use it just like an NSDictionary. To get a value from it, we can call objectForKey:, which will return an Objective-C object, such as an NSString, NSDate, or NSNumber. If we want to retrieve the value as a scalar—like an int, float, or BOOL—we can use another method, such as intForKey:, floatForKey:, or boolForKey:.

When you were creating the property list for this application, you added an array of PreferenceSpecifiers. Some of those specifiers were used to create groups. Others created interface objects that the user used to set the settings. Those are the specifiers we are really interested in, because they hold the real data. Every specifier that was tied to a user setting has a Key named Key. Take a minute to go back and check. For example, the Key for our slider has a value of warpfactor. The Key for our Password field is password. We'll use those keys to retrieve the user settings.

So that we have a place to display the settings, let's quickly set up our main view with a bunch of labels. Before going over to Interface Builder, let's create outlets for all the labels we'll need. Single-click BIDMainViewController.h, and make the following changes:

#import "BIDFlipsideViewController.h"
#define kUsernameKey        @"username"
#define kPasswordKey        @"password"
#define kProtocolKey        @"protocol"
#define kWarpDriveKey       @"warp"
#define kWarpFactorKey      @"warpFactor"
#define kFavoriteTeaKey     @"favoriteTea"
#define kFavoriteCandyKey   @"favoriteCandy"
#define kFavoriteGameKey    @"favoriteGame"
#define kFavoriteExcuseKey  @"favoriteExcuse"
#define kFavoriteSinKey     @"favoriteSin"

@interface BIDMainViewController : UIViewController
        <BIDFlipsideViewControllerDelegate>

@property (weak, nonatomic) IBOutlet UILabel *usernameLabel;
@property (weak, nonatomic) IBOutlet UILabel *passwordLabel;
@property (weak, nonatomic) IBOutlet UILabel *protocolLabel;
@property (weak, nonatomic) IBOutlet UILabel *warpDriveLabel;
@property (weak, nonatomic) IBOutlet UILabel *warpFactorLabel;
@property (weak, nonatomic) IBOutlet UILabel *favoriteTeaLabel;
@property (weak, nonatomic) IBOutlet UILabel *favoriteCandyLabel;
@property (weak, nonatomic) IBOutlet UILabel *favoriteGameLabel;
@property (weak, nonatomic) IBOutlet UILabel *favoriteExcuseLabel;
@property (weak, nonatomic) IBOutlet UILabel *favoriteSinLabel;

- (void)refreshFields;
@end

There's nothing new here. We declare a bunch of constants. These are the key values that we used in our plist file for the different preference fields. Then we declare ten outlets, all of them labels, and create properties for each of them. Finally, we declare a method that will read settings out of the user defaults and push those values into the various labels. We put this functionality in its own method, because we need to do this same task in more than one place.

Save your changes. Now that we have our outlets declared, let's head over to the storyboard file to create the GUI.

Creating the Main View

Select MainStoryboard.storyboard to edit it in Interface Builder. When it comes up, you'll see the main view on the left and the flipside view on the right, connected by a segue. Notice that the background of the main view is dark gray. Let's change it to white.

Single-click the View belonging to the Main View Controller, and bring up the attributes inspector. Use the color well labeled Background to change the background to white. Note that the color well also functions as a popup menu. If you prefer, use that menu to select White Color.

Put the layout area's dock in list mode by clicking the small triangle icon, if it's not already in that mode. In the dock, in the Main View Controller Scene, expand Main View Controller, and then within that, expand View. This reveals an item called Button (see Figure 12–29).

images

Figure 12–29. In the dock, locate the Main View Controller Scene and expand Main View Controller, and then expand View and find the Button item.

TIP: Got a complex Interface Builder list mode hierarchy that you want to open, all at once? Instead of expanding each of the items individually, you can expand the entire hierarchy by holding down the option key and clicking any of the list's disclosure triangles.

The Button, situated at the lower-right corner of the view, contains an icon that's mostly white, and is therefore hard to see against the white background. We're going to change this icon so it will look good on a white background. With the Button selected, bring up the attributes inspector. Change the button's Type from Info Light to Info Dark.

Now we're going to add a bunch of labels to the View so it looks like the one shown in Figure 12–30. We'll need a grand total of 20 labels. Half of them will be static labels that are right-aligned and bold; the other half will be used to display the actual values retrieved from the user defaults and will have outlets pointing to them.

Use Figure 12–30 as your guide to build this view. You don't need to match the appearance exactly, but you must have one label on the view for each of the outlets we declared. Go ahead and design the view. You don't need our help for this. When you're finished and have it looking the way you like, come back, and we'll continue. Just so you know, all our labels used 15-point System Font (or System Font Bold), but feel free to go wild with your own design.

images

Figure 12–30. The View window in Interface Builder showing the 20 labels we added

The next thing we need to do is control-drag from the Main View Controller icon (which represents File's Owner in the storyboard)to each of the labels intended to display a settings value. You will control-drag a total of ten times, setting each label to a different outlet. Once you have all ten outlets connected to labels, save your changes.

Updating the Main View Controller

In Xcode, select BIDMainViewController.m, and add the following code at the beginning of the file:

#import "BIDMainViewController.h"

@implementation BIDMainViewController
@synthesize usernameLabel;
@synthesize passwordLabel;
@synthesize protocolLabel;
@synthesize warpDriveLabel;
@synthesize warpFactorLabel;
@synthesize favoriteTeaLabel;
@synthesize favoriteCandyLabel;
@synthesize favoriteGameLabel;
@synthesize favoriteExcuseLabel;
@synthesize favoriteSinLabel;

- (void)refreshFields {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    usernameLabel.text = [defaults objectForKey:kUsernameKey];
    passwordLabel.text = [defaults objectForKey:kPasswordKey];
    protocolLabel.text = [defaults objectForKey:kProtocolKey];
    warpDriveLabel.text = [defaults boolForKey:kWarpDriveKey]
                              ? @"Engaged" : @"Disabled";

    warpFactorLabel.text = [[defaults objectForKey:kWarpFactorKey]
                            stringValue];
    favoriteTeaLabel.text = [defaults objectForKey:kFavoriteTeaKey];
    favoriteCandyLabel.text = [defaults objectForKey:kFavoriteCandyKey];
    favoriteGameLabel.text = [defaults objectForKey:kFavoriteGameKey];
    favoriteExcuseLabel.text = [defaults objectForKey:kFavoriteExcuseKey];
    favoriteSinLabel.text = [defaults objectForKey:kFavoriteSinKey];
}
.
.
.
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self refreshFields];
}
.
.
.

Also, let's be good memory citizens by inserting the following code into the existing viewDidUnload method:

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.usernameLabel = nil;
    self.passwordLabel = nil;
    self.protocolLabel = nil;
    self.warpDriveLabel = nil;
    self.warpFactorLabel = nil;
    self.favoriteTeaLabel = nil;
    self.favoriteCandyLabel = nil;
    self.favoriteGameLabel = nil;
    self.favoriteExcuseLabel = nil;
    self.favoriteSinLabel = nil;
}

When the user is finished using the flipside view where some preferences can be changed, our controller will be notified of the fact. When that happens, we need to make sure our labels are updated to show any changes. Add the following line of code to the existing flipsideViewControllerDidFinish: method:

- (void)flipsideViewControllerDidFinish:
        (BIDFlipsideViewController *)controller {
    [self refreshFields];
    [self dismissModalViewControllerAnimated:YES];
}

There's not really much here that should throw you. The new method, refreshFields, does nothing more than grab the standard user defaults, and sets the text property of all the labels to the appropriate object from the user defaults, using the key values that we put in our plist file. Notice that for warpFactorLabel, we're calling stringValue on the object returned. All of our other preferences are strings, which come back from the user defaults as NSString objects. The preference stored by the slider, however, comes back as an NSNumber, so we call stringValue on it to get a string representation of the value it holds.

After that, we fleshed out the viewDidAppear: method, where we call our refreshFields method. We call refreshFields again when we are notified that the flipside controller is being dismissed. This will cause our displayed fields to be set to the appropriate preference values when the view loads, and then to be refreshed when the flipside view is swapped out. Because the flipside view is handled modally, with the main view as its modal parent, the BIDMainViewController's viewDidAppear: method will not be called when the flipside view is dismissed. Fortunately, the Utility Applicationtemplate we chose has very kindly provided us with a delegate method we can use for exactly that purpose.

Registering Default Values

We've created a settings bundle, including some default settings for a few values, to give the Settings app access to our app's preferences. We've also set up our own app to access the same information, with a GUI to let the user see and edit it. However, one piece is missing: our app is completely unaware of the default values specified in the settings bundle. You can see this for yourself by deleting the AppSettings app from the iOS simulator or the device you're running on (thereby deleting the preferences stored for the app), and then running it from Xcode again. At the start of a fresh launch, the app will show you blank values for all the settings. Even the default values for the warp drive settings, which we defined in the settings bundle, are nowhere to be seen. If you then switch over to the Settings app, you'll see the default values, but unless you actually change the values there, you'll never see them back in our AppSettings app!

Thereason our setting disappeared is that our app knows nothing about the settings bundle it contains. So, when it tries to read the value from NSUserDefaults for warpFactor and finds nothing saved under that key, it has nothing to show us. Fortunately, NSUserDefaults includes a method called registerDefaults: that lets us specify the default values that we should find if we try to look up a key/value that hasn't been set. To make this work throughout the app, it's best if this is called early during app startup. Select BIDAppDelegate.m, and include this header file somewhere at the top of the file, so we can access the key names we defined earlier:

#import "BIDMainViewController.h"

Then modify the application:didFinishLaunchingWithOptions: method as shown here:

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:
                              [NSNumber numberWithBool:YES], kWarpDriveKey,
                              [NSNumber numberWithInt:5], kWarpFactorKey,
                              @"Greed", kFavoriteSinKey,
                              nil];
    [[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
    return YES;
}

The first thing we do here is create a dictionary containing three key/value pairs, one for each of the keys available in Settings that requires a default value. We're using the same key names we defined earlier, to reduce the risk of mistyping a key name. Then we pass that entire dictionary to the standard NSUserDefaults instance. From that point on, NSUserDefaults will give us the values we specify here, as long as we haven't set different values either in our app or in the Settings app.

This class is complete. You should be able to compile and run your application. It will look something like Figure 12–7, except yours will be showing whatever values you entered in your Settings application, of course. Couldn't be much easier, could it?

Changing Defaults from Our Application

Now that we have the main view up and running, let's build the flipside view. As you can see in Figure 12–31, the flipside view features our warp drive switch, as well as the warp factor slider. We'll use the same controls that the Settings application uses for these two items: a switch and a slider. In addition to declaring our outlets, we'll also declare a method called refreshFields, just as we did in BIDMainViewController, and two action methods that will be triggered by the user touching the controls.

images

Figure 12–31. Designing the flipside view in Interface Builder

Select BIDFlipsideViewController.h, and make the following changes:

#import <UIKit/UIKit.h>

@class BIDFlipsideViewController;

@protocol BIDFlipsideViewControllerDelegate
- (void)flipsideViewControllerDidFinish:(BIDFlipsideViewController *)controller;
@end

@interface BIDFlipsideViewController : UIViewController

@property (weak, nonatomic) id <BIDFlipsideViewControllerDelegate> delegate;
@property (weak, nonatomic) IBOutlet UISwitch *engineSwitch;
@property (weak, nonatomic) IBOutlet UISlider *warpFactorSlider;

- (void)refreshFields;
- (IBAction)engineSwitchTapped;
- (IBAction)warpSliderTouched;
- (IBAction)done:(id)sender;

@end

NOTE: Don't worry too much about the extra code here. As you saw before, the Utility Applicationtemplate makes BIDMainViewController a delegate of the BIDFlipsideViewController. The extra code here that hasn't been in the other file templates we've used implements that delegate relationship.

Now, save your changes and select MainStoryboard.storyboard to edit the GUI in Interface Builder, this time focusing on theFlipside View Controller Scene. Hold down the option key and expand Flipside View Controller and everything below it. Next, double-click the flipside view title in the title bar and change it from Title to Warp Settings.

Next, select the View in the Flipside View Controller Scene, and then bring up the attributes inspector. First, change the background color by using the Background popup to select Light Gray Color. The default flipside view background color is too dark for black text to look good, but light enough that white text is hard to read.

Next, drag two Labels from the library and place them on the View window. Double-click one of them, and change it to read Warp Engines:. Double-click the other, and call it Warp Factor:. You can use Figure 12–31 as a placement guide.

Next, drag over a Switch from the library, and place it against the right side of the view, across from the label that reads Warp Engines. Control-drag from the Flipside View Controller icon to the new switch, and connect it to the engineSwitch outlet. Then control-drag from the switch back to the Flipside View Controller icon, and connect it to the engineSwitchTapped action.

Now drag over a Slider from the library, and place it below the label that reads Warp Factor:. Resize the slider so that it stretches from the blue guideline on the left margin to the one on the right, and then control-drag from the Flipside View Controller icon to the slider, and connect it to the warpFactorSlider outlet. Then control-drag from the slider to Flipside View Controller, and select the warpSliderTouched action.

Single-click the slider if it's not still selected, and bring up the attributes inspector. Set Minimum to 1.00, Maximum to 10.00, and Current to 5.00. Next, select turtle.png for Min Image and rabbit.png for Max Image (you did drag them into the project, right?).

Now, let's finish the flipside view controller. Select BIDFlipsideViewController.m, and make the following changes:

#import "BIDFlipsideViewController.h"
#import "BIDMainViewController.h"

@implementation BIDFlipsideViewController

@synthesize delegate = _delegate;
@synthesize engineSwitch;
@synthesize warpFactorSlider;

.
.
.

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    [self refreshFields];
}

- (void)refreshFields {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    engineSwitch.on = [defaults boolForKey:kWarpDriveKey];
    warpFactorSlider.value = [defaults floatForKey:kWarpFactorKey];

}

- (IBAction)engineSwitchTapped {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setBool:engineSwitch.on forKey:kWarpDriveKey];
}

- (IBAction)warpSliderTouched {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setFloat:warpFactorSlider.value forKey:kWarpFactorKey];
}
.
.
.

Add the following lines of code to the existing viewDidUnload method:

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.engineSwitch = nil;
    self.warpFactorSlider = nil;
}

We added a call to our refreshFields method, whose three lines of code get a reference to the standard user defaults, and then use the outlets for the switch and slider to make them display the values stored in the user defaults.

- (void)refreshFields {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    engineSwitch.on = [defaults boolForKey:kWarpDriveKey];
    warpFactorSlider.value = [defaults floatForKey:kWarpFactorKey];
}

We also implemented the engineSwitchTapped and warpSliderTouched action methods, so that we could stuff the values from our controls back into the user defaults when the user changes them.

Keeping It Real

Now you should be able to run your app, view the settings, and then press the home button and open the Settings app to tweak some values. Hit the home button again, launch your app again, and you may be in for a surprise. If you're running iOS 4.0 or later on your iOS device or simulator (and we bet you are), then when you go back to your app, you won't see the settings change! They'll remain as they are, showing the old values.

When you're using iOS 4, hitting the home button while an app is running doesn't actually quit the app. Instead, the operating system suspends the app in the background, leaving it ready to be quickly fired up again. This is great for switching back and forth between applications, since the amount of time it takes to reawaken a suspended app is much shorter than what it takes to launch it from scratch. However, in our case, we need to do a little more work so that when our app wakes up, it effectively gets a slap in the face, reloads the user preferences, and redisplays the values they contain.

You'll learn more about background applications in Chapter 15, but we'll give you a sneak peek at the basics of how to make your app notice that it has been brought back to life. To do this, we're going to sign up each of our controller classes to receive a notification that is sent by the application when it wakes up from its state of suspended execution.

A notification is a lightweight mechanism that objects can use to communicate with each other. Any object can define one or more notifications that it will publish to the application's notification center, which is a singleton object that exists only to pass these notifications between objects. Notifications are usually indications that some event occurred, and objects that publish notifications include a list of notifications in their documentation. The UIApplication class publishes a number of notifications (you can find them in the Xcode documentation viewer, toward the bottom of the UIApplication page). The purpose of most notifications is usually pretty obvious from their names, but the documentation contains further information if you find one whose purpose is unclear.

Our application needs to refresh its display when the application is about to come to the foreground, so we are interested in the notification called UIApplicationWillEnterForegroundNotification. When we write our viewDidLoad method, we will subscribe to that notification and tell the notification center to call this method when that notification happens. Add this method to both BIDMainViewController.m and BIDFlipsideViewController.m:

- (void)applicationWillEnterForeground:(NSNotification *)notification {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults synchronize];
    [self refreshFields];
}

The method itself is quite simple. First, it gets a reference to the standard user defaults object, and calls its synchronize method, which forces the User Defaults system to save any unsaved changes and also reload any unmodified preferences from storage. In effect, we're forcing it to reread the stored preferences so that we can pick up the changes that were made in the Settings app. Then it calls the refreshFields method, which each class uses to update its display.

Now, we need to make each of our controllers subscribe to the notification we're interested in by adding the following lines to the bottom of the viewDidLoad method in both BIDMainViewController.m and BIDFlipsideViewController.m. Here's the version for BIDMainViewController.m:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    UIApplication *app = [UIApplication sharedApplication];
    [[NSNotificationCenter defaultCenter] addObserver:self
              selector:@selector(applicationWillEnterForeground:)
              name:UIApplicationWillEnterForegroundNotification
              object:app];
}

And here's the version for BIDFlipsideViewController.m:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    [self refreshFields];

    UIApplication *app = [UIApplication sharedApplication];
    [[NSNotificationCenter defaultCenter] addObserver:self
              selector:@selector(applicationWillEnterForeground:)
              name:UIApplicationWillEnterForegroundNotification
              object:app];
}

We start off by getting a reference to our application instance and use that to subscribe to the UIApplicationWillEnterForegroundNotification, using the default NSNotificationCenter instance and a method called addObserver:selector:name:object:. We then pass the following to this method:

  • For an observer, we pass self, which means that our controller class (each of them individually, since this code is going into both of them) is the object that needs to be notified.
  • For selector, we pass a selector to the applicationWillEnterForeground: method we just wrote, telling the notification center to call that method when the notification is posted.
  • The third parameter, name:, is the name of the notification that we're interested in receiving.
  • The final parameter, object:, is the object from which we're interested in getting the notification. If we passed nil for the final parameter, we would get notified any time any method posted the UIApplicationWillEnterForegroundNotification.

That takes care of updating the display, but we also need to consider what happens to the values that are put into the user defaults when the user manipulates the controls in our app. We need to make sure that they are saved to storage before control passes to another app. The easiest way to do that is to call synchronize as soon as the settings are changed, by adding one line to each of our new action methods in BIDFlipsideViewController.m:

- (IBAction)engineSwitchTapped {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setBool:engineSwitch.on forKey:kWarpDriveKey];
    [defaults synchronize];
}

- (IBAction)warpSliderTouched {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setFloat:warpFactorSlider.value forKey:kWarpFactorKey];
    [defaults synchronize];
}

NOTE: Calling the synchronize method is a potentially expensive operation, since the entire contents of the user defaults in memory must be compared with what's in storage. When you're dealing with a whole lot of user defaults at once and want to make sure everything is in sync, it's best to try to minimize calls to synchronize so that this whole comparison isn't performed over and over again. However, calling it once in response to each user action, as we're doing here, won't cause any noticeable performance problems.

There's one more thing to take care of in order to make this work as cleanly as possible. You already know that you must clean up your memory by setting properties to nil when they're no longer in use, as well as performing other cleanup tasks. The notification system is another place where you need to clean up after yourself, by telling the default NSNotificationCenter that you don't want to listen to any more notifications. In our case, where we've registered each view controller to observe this notification in its viewDidLoad method, we should unregister in the matching viewDidUnload method. So, in both BIDMainViewController.m and BIDFlipsideViewController.m, put the following line at the top of the viewDidUnload method:

- (void)viewDidUnload {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
.
.
.
}

Note that it's possible to unregister for specific notifications using the removeObserver:name:object: method, by passing in the same values that were used to register your observer in the first place. But the preceding line is a handy way to make sure that the notification center forgets about our observer completely, no matter how many notifications it was registered for.

With that in place, it's time to build and run the app, and see what happens when you switch between your app and the Settings app. Changes you make in the Settings app should now be immediately reflected in your app when you switch back to it.

Beam Me Up, Scotty

At this point, you should have a very solid grasp on both the Settings application and the User Defaults mechanism. You know how to add a settings bundle to your application and how to build a hierarchy of views for your application's preferences. You also learned how to read and write preferences using NSUserDefaults, and how to let the user change preferences from within your application. You even got a chance to use a new project template in Xcode. There really shouldn't be much in the way of application preferences that you are not equipped to handle now.

In the next chapter, we're going to show you how to keep your application's data around after your application quits. Ready? Let's go!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.140.252.192