Chapter 10. 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 has 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 we'll show you how to access those settings from within your application.

Getting to Know Your Settings Bundle

The Settings application (see Figure 10-1) lets the user enter and change preferences for any application that has a settings bundle. A settings bundle is a group of files built into an application that tells the Settings application what preferences the application wishes to collect from the user.

Pick up your iPhone or iPod touch, and locate your Settings icon. You'll find it on the home screen. When you touch the icon, the Settings application will launch. Ours is shown in Figure 10-2.

The Settings application icon is the third one down in the last column. It may be in a different spot on your iPhone or iPod touch, but it's always available.

Figure 10.1. The Settings application icon is the third one down in the last column. It may be in a different spot on your iPhone or iPod touch, but it's always available.

The Settings application

Figure 10.2. The Settings application

The Settings application acts as a common user interface for the iPhone's User Defaults mechanism. User Defaults is the part of Application Preferences that stores and retrieves preferences. 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 you don't have to design a 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. There are limits to what you can do with the Settings application, however. Any preference that the user might need to change while your application is running should not be limited to the Settings application because your user would be forced to quit your application to change those values.

Immersive applications, such as games, generally should provide their own preferences view so that the user doesn't have 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 right in your application and store those in iPhone's User Defaults.

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 (see Figure 10-3).

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

You should also notice that there are two items on the view that have disclosure indicators. The first one, Protocol, takes the user to another table view that displays the available options for that item. From that table view, the user can select a single value (see Figure 10-5).

The settings application showing an entry for our application in the simulator

Figure 10.3. The settings application showing an entry for our application in the simulator

Our application's primary settings view

Figure 10.4. Our application's primary settings view

Selecting a single preference item from a list

Figure 10.5. Selecting a single preference item from a list

The other disclosure indicator on our application's main view in the Settings application allows the user to drill down to another set of preferences (see Figure 10-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.

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

In order to show how to update preferences from within our application, we also provide a little information button in the lower-right corner that will take the user to another view to set two of the preference values right in our application (see Figure 10-8).

A child settings view in our application

Figure 10.6. A child settings view in our application

Our application's main view

Figure 10.7. Our application's main view

Setting some preferences right in our application

Figure 10.8. Setting some preferences right in our application

Let's get started, shall we?

Creating the Project

In Xcode, press

Creating the Project

This is a new project template that we haven't used before, so let's take a second to look at the project before we proceed. This template creates an application very similar to the one 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.

You'll notice that, for the first time, there is no Classes folder in our Xcode project (see Figure 10-9). Because it takes several files to implement this type of application, the template very kindly organizes the files in groups for us to make our lives easier. Expand the folders Main View, Flipside View, and Application Delegate. Heck, while you're in the folder-expanding groove, flip open Resources too.

Our project created from the Utility Application template

Figure 10.9. Our project created from the Utility Application template

All the classes that make up the main view, including the view controller and a subclass of UIView, are included in the folder called Main View. Likewise, all source code files needed to implement the flipside view are contained in the folder called Flipside View. Finally, the application delegate is contained in a folder called (wait for it...) Application Delegate.

This template has provided us with a custom subclass of UIView for both the main and flipside views. We won't actually need to subclass UIView in this application for either of our views, but we'll leave both FlipsideView and MainView in our project. It won't hurt anything to leave them as is, but if we remove them, we will have to go rewire the nibs to point to UIView.

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 in a few minutes. If it finds a settings bundle with an appropriate Root.plist file, the Settings application will build a settings view for our application based on the contents of the property list. If we want our preferences to include any subviews, we have to add additional 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.

One small wrinkle with this process is that you can't add or delete items from a settings bundle from within Xcode. You can change the contents of files that are already in the settings bundle from Xcode, but if you need to actually add or remove items, you'll have to do it in the Finder. No worries, we'll show you how to do this a bit further down.

Adding a Settings Bundle to Our Project

In the Groups & Files pane, click the root object (the one called AppSettings, which should be at the very top of the list) and then select New File... from the File menu or press

Adding a Settings Bundle to Our Project
Creating a settings bundle

Figure 10.10. Creating a settings bundle

You should now see a new item in Xcode's Groups & File pane called Settings.bundle. Expand Settings.bundle, and you should see two items, an icon named Root.plist and a folder named en.lproj. We'll discuss en.lproj in Chapter 17 when we talk about localizing your application into other languages. For the moment, let's just concentrate on Root.plist.

Setting Up the Property List

Single-click Root.plist, and take a look at the editor pane. You're looking at Xcode's property list editor. This editor functions in the same way as the Property List Editor application in /Developer/Applications/Utilities.

Property lists all have a root node, which has a node type of Dictionary, which means it stores items using a key value, just as an NSDictionary does. All of the children of a Dictionary node need to have both a key and a value. There can be only one root node in any given property list, and all nodes must come under it.

There are several different types of nodes that can be put into a property list. In addition to Dictionary nodes, which allow you to store other nodes under a key, there are also 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. There are also a number of other node types designed to hold data. The data node types are Boolean, Data, Date, Number, and String.

Tip

Although you can use most kinds of objects as a key in an NSDictionary, keys in property list dictionary nodes have to be strings, though you are free to use any node type for the values.

When creating a settings property list, you have to follow a defined format. Fortunately, when you added the settings bundle to your project, a properly formatted property list, called Root.plist, was created for you. This is the Root.plist that you just clicked in the settings bundle.

In the Root.plist editor pane, expand the node named PreferenceSpecifiers (see Figure 10-11).

Root.plist in the editor pane

Figure 10.11. Root.plist in the editor pane

Before we add our preference specifiers, let's look at the property list so you can see the required format. We'll talk about the first item, StringsTable, in Chapter 17 as well; a strings table is also used in translating your application into another language. Since it is optional, you can delete that entry now by clicking it and pressing the delete key. You can leave it there if you like, since it won't do any harm.

The next item under the root node is PreferenceSpecifiers, and it's an array. Click its disclosure triangle to reveal its subitems. This array node is designed to hold a set of dictionary nodes, each of which represents 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. Those nodes aren't likely to reflect our actual preferences, so delete Item 2, Item 3, and Item 4 by single-clicking each of those rows and pressing the delete key.

Single-click Item 1 but don't expand it. Look at the right edge of the row, and notice the button with the plus icon. That button is used to add a sibling node after this row. In other words, it will add another node at the same level as this one. If we click that icon now (don't click it, just follow along), we will get a new row called Item 2 right after Item 1.

Now expand Item 1, and notice that the button changes to a different icon, one with three horizontal lines. That new icon indicates that clicking that button now will add a child node, so if we click it now (again, don't click it, just follow along), we will get a new row underneath Item 1.

The first row under Item 1 has a key of Type, and every property list node in the PreferenceSpecifiers array must have an entry with this key. It's typically the first one, but order doesn't matter in a dictionary, so the Type key doesn't have to be first. The Type key tells the Settings application what type of data is associated with this item.

Take a look at the Type field under Item 1. The value of this Type field, PSGroupSpecifier, is used to indicate that this 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 10-4, you'll see that the Settings application presents the settings in a grouped table. Item 1 in the PreferenceSpecifiers array in a settings bundle property list should always be a PSGroupSpecifier so the settings start in a new group, because you need at least one group in every Settings table.

The only other entry in Item 1 has a key of Title, and this is used to set an optional header just above the group that's being started. If you look again back at Figure 10-4, you'll see that our first group is called General Info. Double-click the value next to Title, and change it 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 we single-click the PreferenceSpecifiers row in the editor pane, and click the button 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 do this, click the disclosure triangle to the left of Item 1 to close it, and then select Item 1 and click the plus button at the end of the row, which will give us a new sibling row after the current row (see Figure 10-12).

Adding a new sibling row to Item 1

Figure 10.12. Adding a new sibling row to Item 1

The new row will default to a String node type, which is not what we want. Remember, each item in the PreferenceSpecifiers array has to be a dictionary, so click the word String, and change the node type to Dictionary. Now, click the disclosure triangle next to Item 2 to expand it. It doesn't actually contain anything yet, so the only differences you'll see are that the disclosure triangle will point down and the button to add sibling nodes will change to let you add child nodes. Click the add child node button (the button to the right with three lines) now to add our first entry to this dictionary.

A new row will come up and default to a String type, which is what we want. The new row's key value will default to New item. Change it to Type, and then double-click the Value column, and enter PSTextFieldSpecifier, which is the type value used to tell the Settings application that we want the user to edit this setting in a text field.

In this example, PSTextFieldSpecifier is a type. More specifically, it is the type of a specific preference field. When you see Type in the Key column, we're defining the type of field that will be used to edit the preference.

Click the button with the plus icon to the right of the Type row to add another item to our dictionary. This next row will specify the label that will be displayed next to the text field. Change the key from New item to Title. Now press the tab key. Notice that you are now all set to edit the value in the Value column. Set it to Username. Now press the plus button at the end of the Title row to add yet another item to our dictionary.

Change the key for this new entry to Key (no, that's not a misprint, you're really setting the key to "Key"). For a value, type in username. Recall that we said that user defaults work like a dictionary? Well, this entry tells the Settings application what 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 had a value of Username and our Key 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 a 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 what convention you use for your preference keys.

Add another item to our 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.

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. If you did want the text field to use autocorrection, then you would change the value in this row to Yes. When you're all done, your property list should look like the one shown in Figure 10-13.

The finished text field specified in Root.plist

Figure 10.13. The finished text field specified in Root.plist

Save the property file, and let's see if everything is set up and working. We should be able to compile and run the application now. Even though our application doesn't do anything yet, we should be able to click the home button on the iPhone simulator, and then select the Settings application to see an entry for our application (see Figure 10-3).

Try it now by selecting Build and Run from the Build menu. If you click the home button and then the icon for the Settings application, you should find an entry for our application, which uses the application icon we added earlier. If you click the AppSettings row, you should be presented with a simple settings view with a single text field, as shown in Figure 10-14.

Our root view in the Settings application after adding a group and a text field

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

Adding a Secure Text Field Setting

Quit the simulator, and go back to Xcode. We're not done yet, but you should now have a sense of how easy adding preferences to your application is. 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.

Here's an easy way to add another node. Collapse Item 2 in the PreferenceSpecifiers array. Now select Item 2. Press

Adding a Secure Text Field Setting

Next, add one more child to the new item. Remember, the order of items does not matter, so feel free to place it right below the Key item. Give the new item a Key of IsSecure, and change the Type to Boolean. Once you do that, the space where you normally type in a value will change to a checkbox. Click it to check the box, which tells the Settings application that this field needs to be a password field rather than just an ordinary text field.

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, and clicking it will take you down to another table where you can select one of several rows. Collapse Item 3; select the row; and click the plus icon at the end of the row to add Item 4. Change Item 4's Type to Dictionary, and expand Item 4 by clicking the disclosure triangle.

Give it a child row with a key of Type and a value of PSMultiValueSpecifier. Add a second row with a key of Title and value of Protocol. Now create a third row with a key of Key and 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 4, but they are going to be Array type nodes, not String type nodes. One, called Titles, is going to hold a list of the values that the user can select from. The other, called Values, is going to hold a list of the values that actually get 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, a date, or a 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.

Add a new child to Item 4. Change its key to Values and set its type to Array. Expand the array, and add five child nodes. All five nodes should be String type nodes and should contain the following values: HTTP, SMTP, NNTP, IMAP, and POP3.

Tip

Note that if you enter the first value and press return, you'll be editing the value just beneath it. Shortcut!

Once you've entered all five, collapse Values, and select it. Then, press

Adding a Multivalue Field

We're almost done 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 have 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). Add another child to Item 4. Give it a key of DefaultValue and a value of SMTP.

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

Three fields down

Figure 10.15. Three fields down

Adding a Toggle Switch Setting

The next item we need to get from the user is a Boolean value that indicates whether the 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 PreferenceSpecifiers array with a type of PSToggleSwitchSpecifier.

Collapse Item 4 if it's currently expanded, and then single-click it to select it. Click the plus icon at the right side of the row to create Item 5. Change its type to Dictionary, and then expand Item 5, and add a child row. Give the child row a key of Type and a value of PSToggleSwitchSpecifier. Add another child row with a key of Title and a value of Warp Drive. Next, add a third child row with a key of Key and a value of warp.

By default, a toggle switch will cause a Boolean YES or NO to get saved into the user defaults. If you would prefer to assign a different value to the on and off positions, you can do that by specifying the optional keys TrueValue and FalseValue. You can assign strings, dates or numbers to either the on position (TrueValue) or the off position (FalseValue) so that the Settings application will store the string you specify instead of just storing YES or NO. Let's set the on position to save the string Engaged and the off position to store Disabled.

Do this by adding two more children to Item 5, one with a key of TrueValue and a value of Engaged, and a second one with a key of FalseValue and a value of Disabled.

We have one more required item in this dictionary, which is the default value. If we had not supplied the option FalseValue and TrueValue items, we would create a new row with a key of DefaultValue and change the type from String to Boolean. However, because we did add those two items, the value we put in DefaultValue has to match either the value passed in TrueValue or the one passed in FalseValue.

Let's make our warp engines on by default, so create one last child to Item 5, give it a key of DefaultValue and a value of Engaged. Note that the string "Engaged" is what will be stored in the user defaults, not what will appear on the screen. We just wanted to be clear on that.

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.

Single-click Item 1 under PreferenceSpecifiers, and press

Adding the Slider Setting

Expand Item 6, double-click the value in the row labeled Title and change the value to Warp Factor.

Collapse Item 6 and select it. Then, click the button at the end of its row to add a new sibling row. Change the Type of the new row, Item 7, from String to Dictionary and then expand the new row. Add a child row, and give it a key of Type and a value of PSSliderSpecifier, which indicates to the Settings application that it should use a UISlider to get this information from the user. Add another child with a key of Key and a value of warpFactor so that the Settings application knows what key to use when storing this value.

We're going to allow the user to enter a value from one to ten, and we'll set the default to warp 5. 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. To do this, add three more child rows to Item 7, setting the Type of all three rows from String to Number. Give the first one a key of DefaultValue and a value of 5. Give the second one a key of MinimumValue and a value of 1, and give the final one a key of MaximumValue and 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. Sliders allow placement of a small 21-pixel × 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.

In the 10 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 Resources folder, we need to put them in the settings bundle so the Settings application can get them. To do that, go to the Finder and navigate to wherever you saved your Xcode project. In that same folder, you'll find an icon named Settings.bundle.

Remember, bundles look like files in the Finder, but they are really folders, and you can get to their contents by right-clicking (or control-clicking) the bundle's icon and selecting Show Package Contents. This will open a new 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 10 AppSettings folder to this folder.

You can leave this window open in the Finder, as we'll need to copy another file here in a few minutes. For now, go back to Xcode, and let's tell the slider to use these two images.

Go back to Root.plist and add two more child rows under Item 7. 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. Save your property list, and let's build and run to make sure everything is still hunky-dory. If everything is, 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 of the slider (see Figure 10-16).

We have text fields, multivalue fields, a toggle switch, and a slider. We're almost done.

Figure 10.16. We have text fields, multivalue fields, a toggle switch, and a slider. We're almost done.

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. Before we add that node, however, since we don't want this new preference to be grouped with the slider, we're going to copy the group specifier in Item 1 and paste it at the end of the PreferenceSpecifiers array to create a new group for our child settings view. In Root.plist, collapse Item 1 if it's expanded, and then single-click it to select it and press

Adding a Child Settings View

Now, collapse Item 8 again. Select it, and press the add sibling button at the right end of the row to add Item 9, which will be our actual child view. Change the new row's type from String to Dictionary and expand it by clicking the disclosure triangle. Add a child row, and give it a key of Type and a value of PSChildPaneSpecifier. Add another child row with a key of Title and a value of More Settings.

We need to add one final row, 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. The file extension .plist is assumed and must not be included, or the Settings application won't find the property list file.

We are adding a child view to our main preference view. That settings in that child view are specified in the More.plist file. We need to copy More.plist into the 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 have 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 property list file, so to save yourself some typing, why don't you grab More.plist out of the 10 AppSettings folder in the projects archive that accompanies this book, and drag it into that Settings.bundle window we left open earlier.

Tip

When you create your own child settings views, the easiest way to do it 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 to that new file.

We're done with our settings bundle. Feel free to compile, run, and test out 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. We've covered almost every configuration option available (at least at the time of this writing), but you can find the full documentation of the settings property list format in the document called Settings Application Schema Reference in the iPhone Dev Center. You'll find it on this page, along with a ton of other useful reference documents:

http://developer.apple.com/iphone/library/navigation/Reference.html

Before we continue on, we've included an application icon with this chapter's code to make sure your program looks like ours. First, open the 10 AppSettings folder in the project archive, grab the three image files there (icon.png, rabbit.png, and turtle.png) and add them to the Resources folder of your project. Then, make icon.png your application icon by single-clicking AppSettings-Info.plist in the Resources folder, and setting the value of the Icon file row to icon.png. Be sure to save AppSettings-Info.plist when you are done.

Note

You might have noticed that two of the icons you just added are exactly the same ones you added to your settings bundle earlier, and you might be wondering why. Remember: Applications on the iPhone 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 Resources folder so they get 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.

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 out of it, we can call objectForKey: which will return an Objective-C object like an NSString, NSDate, or NSNumber. If we want to retrieve the value as a scalar like an int, float, or BOOL, we can use other methods, such as intForKey:, floatForKey:, or boolForKey:.

When you were creating the property list for this application, you created an array of PreferenceSpecifiers. Some of those specifiers were used to create groups. Others created interface objects that the user used to set their settings. Those are the specifiers we are really interested in, because that's where the real data is. Every specifier that was tied to a user setting had a Key named Key. Take a minute to go back and check. For example, the Key for our slider had a value of warpfactor. The Key for our Password field was 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 MainViewController.h, and make the following changes:

#import "FlipsideViewController.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 MainViewController : UIViewController
        <FlipsideViewControllerDelegate> {
    UILabel *usernameLabel;
    UILabel *passwordLabel;
    UILabel *protocolLabel;
    UILabel *warpDriveLabel;

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

- (void)refreshFields;
- (IBAction)showInfo;
@end

There's nothing new here. We declare a bunch of constants. These are the key values that we used in our property list 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 have to do this same task in more than one place. Now that we've got our outlets declared, let's head over to Interface Builder.

Double-click MainView.xib to open it in Interface Builder. When it comes up, you'll notice that the background of the view is dark gray. Let's change it to white. Single-click the Main View icon in the nib's main window, and press

Reading Settings in Our Application

Put the main window (the one titled MainView.xib) in list mode (the center View Mode button). Next, click the disclosure triangle to the left of the Main View icon. This reveals an icon called Light Info Button (see Figure 10-17).

Using the list view mode

Figure 10.17. Using the list view mode

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.

We're going to change this icon so it will look good on a white background. Single-click the Light Info Button icon to select it, and then press

Using the list view mode

Now we're going to add a bunch of labels to the Main View so it looks like the one shown in Figure 10-18. We'll need a grand total of twenty 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 10-18 as your guide to build this view. You don't have to match the appearance exactly, but you do need to 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 done and have it looking the way you like, come back, and we'll continue on.

The Main View window in Interface Builder

Figure 10.18. The Main View window in Interface Builder

The next thing we need to do is control-drag from File's Owner 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, close the MainView.xib window, and go back to Xcode.

Single-click MainViewController.m, and add the following code at the beginning of the file.

#import "MainViewController.h"
#import "MainView.h"

@implementation MainViewController
@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 objectForKey:kWarpDriveKey];
    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 {
    [self refreshFields];
    [super viewDidAppear:animated];
}
...

Also, let's be a good memory citizen by inserting the following code into the existing dealloc and viewDidUnload methods:

- (void)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;
    [super viewDidUnload];
}
- (void)dealloc {
    [usernameLabel release];
    [passwordLabel release];
    [protocolLabel release];
    [warpDriveLabel release];
    [warpFactorLabel release];
    [favoriteTeaLabel release];
    [favoriteCandyLabel release];
    [favoriteGameLabel release];
    [favoriteExcuseLabel release];
[favoriteSinLabel release];
    [super dealloc];
}
...

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

- (void)flipsideViewControllerDidFinish:
        (FlipsideViewController *)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 properties 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 added a viewDidAppear: method, where we call our refreshFields method. We call refreshFields again when we get notified that the flipside controller is being dismissed. This will cause our displayed fields to get set to the appropriate preference values when the view loads, and then to get refreshed when the flipside view gets swapped out. Because the flipside view is handled modally with the main view as its modal parent, MainViewController's viewDidAppear: method will not get called when the flipside view is dismissed. Fortunately, the Utility Application template we chose has very kindly provided us with a delegate method we can use for exactly that purpose.

This class is done. You should be able to compile and run your application and have it look something like Figure 10-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've got the main view up and running, let's build the flipside view. As you can see in Figure 10-19, 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. First, we need to declare our outlets, so single-click FlipsideViewController.h, and make the following changes:

@protocol FlipsideViewControllerDelegate;

@interface FlipsideViewController : UIViewController {
    id <FlipsideViewControllerDelegate> delegate;
    UISwitch *engineSwitch;
    UISlider *warpFactorSlider;
}

@property (nonatomic, assign) id <FlipsideViewControllerDelegate> delegate;
@property (nonatomic, retain) IBOutlet UISwitch *engineSwitch;
@property (nonatomic, retain) IBOutlet UISlider *warpFactorSlider;

- (IBAction)done;
@end

@protocol FlipsideViewControllerDelegate
- (void)flipsideViewControllerDidFinish:
     (FlipsideViewController *)controller;
@end

Note

Don't worry too much about the extra code here. As we saw before, the Utility Application template makes MainViewController a delegate of the FlipsideViewController, the extra code here that hasn't been in the other file templates we've used implements that delegate relationship.

Now, double-click FlipsideView.xib to open it in Interface Builder. If the Flipside View window is not open, double-click the Flipside View icon in the nib's main window to open it. First, change the background color using the attribute inspector to a lighter shade of gray, about a 25% gray should work well. 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 Flipside 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 10-19 as a placement guide.

Desiging the flipside view in Interface Builder

Figure 10.19. Desiging the flipside view in Interface Builder

When you're done placing the controls, double-click the word Title at the top of the view and change it to read Warp Settings.

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 File's Owner icon to the new switch, and connect it to the engineSwitch outlet.

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 guide line on the left margin to the one on the right, and then control-drag from the File's Owner icon to the slider, and connect it to the warpFactorSlider outlet.

Single-click the slider if it's not still selected, and press

Desiging the flipside view in Interface Builder
The attributes inspector for our Warp Factor slider

Figure 10.20. The attributes inspector for our Warp Factor slider

Save and close the nib, and head back to Xcode so we can finish the flipside view controller. Single-click FlipsideViewController.m, and make the following changes:

#import "FlipsideViewController.h"
#import "MainViewController.h"

@implementation FlipsideViewController
@synthesize delegate;
@synthesize engineSwitch;
@synthesize warpFactorSlider;

- (void)viewDidLoad {
    
The attributes inspector for our Warp Factor slider
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; engineSwitch.on = ([[defaults objectForKey:kWarpDriveKey] isEqualToString:@"Engaged"]) ? YES : NO; warpFactorSlider.value = [defaults floatForKey:kWarpFactorKey]; [super viewDidLoad]; }
- (void)viewWillDisappear:(BOOL)animated {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *prefValue = (engineSwitch.on) ? @"Engaged" : @"Disabled";
    [defaults setObject:prefValue forKey:kWarpDriveKey];
    [defaults setFloat:warpFactorSlider.value forKey:kWarpFactorKey];
    [super viewWillDisappear:animated];
}
...

Add the following lines of code to the existing dealloc and viewDidUnload methods:

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

In the viewDidLoad method, we deleted one line of code and added three (well, four, because one line was too long to fit the page width of this book). The one line of code we deleted wasn't really important. Code in the template set the background color of the view using a class method, and that line of code caused the flipside view to have a textured, dark gray appearance rather than using the background that was set in Interface Builder. The textured background made it difficult to read the text and to see the slider pictures that we used; we deleted it to let the background color from Interface Builder shine through so our text and icons could be seen more easily.

The four lines of code we added get a reference to the standard user defaults and use the outlets for the switch and slider to set them to the values stored in the user defaults. Because we opted to store strings rather than Booleans for the warp drive setting, we have to handle the conversion in our code because a UISwitch instance is set using a BOOL property.

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
engineSwitch.on = ([[defaults objectForKey:kWarpDriveKey]
     isEqualToString:@"Engaged"]) ? YES : NO;
warpFactorSlider.value = [defaults floatForKey:kWarpFactorKey];

We also overrode our parent's viewWillDisappear: method so that we could stuff the values from our controls back into the user defaults before the main view is shown again. Because our controller's viewDidDisappear: method will fire before the main view's viewWillAppear: method, the changed values will already be stored in the user defaults for the view to retrieve, so the main view will get updated with the correct new values.

Beam Me Up, Scotty

At this point, you should have a very solid grasp on both the Settings application and user defaults. 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 saw how to read and write preferences using NSUserDefaults and how to let the user change preferences from within your application, and 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 aren't equipped to handle now.

In the next chapter, we're going to tackle the different approaches to file management on the iPhone. We'll cover different techniques for persisting your objects to the file system and also take a look at using your iPhone's embedded database, SQLite. You'll also get your first look at a very cool technology called Core Data. 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.145.66.241