Users look for application preferences in two main settings:
in the application itself, and in the iPhone’s Settings application. For simple applications, applications
with few preferences, and applications with preferences that need to be
modified regularly, you should keep the preferences within the application
itself. However, for more complicated applications, applications with
complicated or numerous different preferences, and applications with
preferences that the user will rarely have to modify, it’s preferable to
use the Settings application.
Warning:
Despite it being done in some applications currently for sale on
the App Store, Apple advises that you should never split your
preferences between the Settings application and a custom settings
screen inside your own application. According to Apple, “If you have
preferences, pick one solution and use it exclusively.” This is good
advice; having multiple places to change settings is confusing not just
for the user, but also for you as a developer.
Adding a preferences panel for your application to the main Settings
application is easy. You do this by adding a special Settings.bundle file to your
application and then configuring the Root.plist file
contained inside the bundle in the Xcode editor.
When the built-in Settings application launches, it checks each
third-party application for the presence of a Settings Bundle. For each
bundle it finds, it displays the application’s name and icon on the main
page. When the user taps the row belonging to the application, Settings
loads the Root.plist Settings Page file and uses that
file to display your application’s main page of preferences.
Open the
WhereAmI project in Xcode, right-click on the
project’s icon in the Groups & Files pane in Xcode, and select
Add→New File. In the pop-up window that
appears, click on the Resource category in the lefthand pane underneath
iPhone OS, select Settings Bundle, as shown in Figure 1, and click Next.
Accept the default suggested name of Settings.bundle
when prompted.
You’ll notice that the bundle appears in the Groups & Files pane
in Xcode with an icon that looks a lot like a Lego brick. If you click on
the arrow beside it to expand the bundle you’ll see the
Root.plist file that contains an XML description of
the settings root page, and an en.lproj directory
containing the localized string resource file (for English). You can add
further localizations to your Settings Bundle if needed.
The default Settings Bundle contains some example settings. Click on
the Build and Run button in the Xcode toolbar to compile and deploy the
application into iPhone Simulator. Tap the simulator’s Home button to quit
out of the application, and then find the Settings application on the Home
screen. Tap the Settings application to open it, and you should see
something similar to Figure 2.
Since we haven’t added an icon to the application , the space to the left of the
WhereAmI entry is blank; if we had added an icon it would be displayed
next to our application name. If you now tap the WhereAmI entry, you’ll be
presented with the default preferences pane generated from the Settings
Bundle, also shown in Figure 12-2.
Note:
If a file called Icon-Settings.png (a 29×29-pixel image) is located at the top level of your
application’s bundle directory (drag it into the top level of your
project under Groups & Files and check the box to copy the item),
that icon is used to identify your application preferences in the
Settings application. If no such image is present, the Settings
application uses a scaled down version of your application’s icon file
instead.
Returning to Xcode, click on the Root.plist
file inside Settings.bundle to open it in the Xcode
editor, and you’ll see the property list description of the Settings page.
Option-click the disclosure triangle next to PreferencesSpecifiers, and
you’ll see all the settings, as shown in Figure 3.
Note:
Like any property list file, Xcode by default displays the
Root.plist file as a key-value pair list. However,
you can see the raw XML of the Root.plist property
list by right-clicking on the Root key and selecting Open As→Source Code File.
If you compare Figures Figure 2 and Figure 3, you can see how the property list file
(Figure 3) compares to
the rendered user interface (Figure 2):
Item 0 (PSGroupSpecifier) is
a group label whose value is the string Group.
Item 1 (PSTextFieldSpecifier)
is a text label whose value is the string Name.
Item 2 (PSToggleSwitchSpecifier) is a toggle switch
labeled “Enabled” with a default value of YES.
Item 3 (PSSliderSpecifier) is
a slider bar with a minimum value of 0, a maximum value of 1, and a
default value of 0.5.
Each UI element is an item described in the PreferenceSpecifiers array.
To make this easier to work with, you can tell Xcode explicitly that
the Root.plist file is an iPhone Settings bundle.
From the Xcode menu select View→Property
List Type→iPhone Settings plist (you may
need to double-click on Root.plist before this option becomes available on
the menu). This tells Xcode to format the contents of the property list a
little differently, as shown in Figure 4, making it easier to
understand and edit.
There are six possible property list keys:
Group (PSGroupSpecifier)
Title (PSTitleValueSpecifier)
Text Field (PSTextFieldSpecifier)
Toggle Switch (PSToggleSwitchSpecifier)
Multi Value (PSMultiValueSpecifier)
Slider (PSSliderSpecifier)
Additionally, although we won’t go into it here, you can point to
child preference panes (additional settings pages) using the Child Pane
(PSChildPaneSpecifier) property list
key.
Let’s modify the default property key list provided by Xcode.
Click on Item 3 and press the Backspace key to delete it from the
property list file; do the same for Item 1. You should be left with a
Group and a Toggle Switch.
Rename the Group: under Item 0, double-click on the Title property’s
value and enter Latitude & Longitude.
Keep the Toggle Switch unmodified. After doing this, the
Root.plist file should resemble Figure 5.
Make sure you’ve saved your changes to the
Root.plist file and click the Build and Run button in
the Xcode toolbar. Once the application has started, tap the Home button
and make your way to the Settings application. Tap the WhereAmI preference
entry, and you should now see something closely resembling Figure 6. We’re going to use
the preference pane to toggle whether we want the application to display
the latitude and longitude on the screen when it displays our map.
Note:
When you run your application in iPhone Simulator, it stores
preference values in ~/Library/Application Support/iPhone
Simulator/User/Applications/<APP_ID>/Library/Preferences,
where <APP_ID> is a randomly generated
directory name. However, each time Xcode performs a clean install of
your application, any previous version of the application’s preferences
will be deleted.
Return to Xcode and click on the
WhereAmIAppDelegate.m file to open it in the Xcode
editor. Now add the following class method, which initializes the default
settings for the application:
+ (void)initialize {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *defaultsToRegister =
[NSDictionary dictionaryWithObject:@"YES"
forKey:@"enabled_preference"];
[defaults registerDefaults:defaultsToRegister];
}
If your user has already accessed the application’s settings inside
the iPhone Settings application, the default settings will already have
been initialized. If this has not been done, the values will not exist and
will be set to nil (or in the case of
Booleans, to NO). As the application
delegate is loaded, this method initializes the user defaults (the
initialize: message is sent to each
class before it receives any other messages).
Using this method to set the defaults has the unfortunate side
effect that you have to specify your defaults in two places: in the
Root.plist file, where they properly belong; and in
your application delegate, where they don’t.
The right way to deal with this problem is to read in the defaults
from the Settings.bundle file which is stored as part
of your application. To do this, replace the initialize: method with the following:
+ (void)initialize {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *settingsBundle =
[[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
NSDictionary *settings =
[NSDictionary dictionaryWithContentsOfFile:
[settingsBundle stringByAppendingPathComponent:@"Root.plist"]];
NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"];
NSMutableDictionary *defaultsToRegister =
[[NSMutableDictionary alloc] initWithCapacity:[preferences count]];
[defaults registerDefaults:defaultsToRegister];
}
If your application preferences don’t exist when your application is
launched, you can therefore read the values directly from the
Settings.bundle file rather than having to store the
defaults in two places.
You can check that your preference bundle is working correctly by
adding the following into the application delegate’s applicationDidFinishLaunching: method and
checking the Console (select Run→Console
from the Xcode menu bar). Add the lines shown in bold:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL enabled = [defaults boolForKey:@"enabled_preference"];
NSLog(@"enabled = %d", enabled);
self.locationManager = [[[CLLocationManager alloc] init] autorelease];
... other code not shown ...
}
We may have working preferences, but they don’t do anything yet.
Let’s change that right now. Click on the
WhereAmIViewController.h interface file to open it in
the Xcode editor, and add the following outlets to the declaration (inside
the curly braces of the @interface
block):
IBOutlet UIButton *backgroundButton;
IBOutlet UILabel *latLabel;
IBOutlet UILabel *longLabel;
There is no need to make them properties.
Make sure you’ve saved all your changes (Option-⌘-S), and then
double-click on the WhereAmIViewController.xib NIB
file to open it in Interface Builder. Click on File’s Owner, and in the
Connections tab of the Inspector window connect the backgroundButton outlet to the UIButton we used as a background for the labels,
as shown in Figure 7;
then connect the latLabel and longLabel outlets to the “Latitude” and
“Longitude” UILabels,
respectively.
Save your changes to the NIB file and return to Xcode. Then click on
the WhereAmIViewController.m implementation file to
open it in the Xcode editor. Add the following viewWillAppear: method:
- (void)viewWillAppear:(BOOL)animated {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ( [defaults boolForKey:@"enabled_preference"] ) {
backgroundButton.hidden = NO;
latLabel.text = @"Latitude";
longLabel.text = @"Longitude";
} else {
backgroundButton.hidden = YES;
latLabel.text = @"";
longLabel.text = @"";
}
[super viewWillAppear:animated];
}
This method checks the application preferences to see if Latitude
& Longitude are enabled. If they are, we set the text of the labels
appropriately and make sure the button is visible. Correspondingly, if
Latitude & Longitude are disabled, we hide the button and empty both
strings.
Finally, we have to go back into the application delegate file and
make a small modification to the locationManager:didUpdateToLocation:fromLocation: method. Here we have to stop the application from printing
the current latitude and longitude to the screen if Latitude &
Longitude are disabled via preferences. Add the lines shown in bold
(wrapping the two existing assignments):
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ( [defaults boolForKey:@"enabled_preference"] ) {
viewController.latitude.text =
[NSString stringWithFormat:@"%f", newLocation.coordinate.latitude];
viewController.longitude.text =
[NSString stringWithFormat:@"%f", newLocation.coordinate.longitude];
}
This brackets the lines that set the text of the UILabels with an if() block; we set the text of the labels only
if Latitude & Longitude are enabled in the preferences.
We’re done here. Make sure all of your changes have been saved, and
click the Build and Run button in the Xcode toolbar to compile and deploy
your application into iPhone Simulator.
By default, the Latitude & Longitude display is enabled, so
everything should appear as before. However, if you disable Latitude &
Longitude in Settings, quit out of Settings, and relaunch the Where Am I?
application, you’ll see that Latitude & Longitude has disappeared, as
shown in Figure 8.
1. Accessing Global Preferences
As well as your own application preferences, you can programmatically access the device’s global
preferences from your own application:
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:
@"Library/Preferences/.GlobalPreferences.plist"];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
NSLog(@"Phone number: %@", [dict objectForKey:@"SBFormattedPhoneNumber"]);
While there are a number of entries, the two keys that are
probably going to be of most interest to you as a developer are AppleLocale
and SBFormattedPhoneNumber. These are the current
localizations used by the device; as I am based in the United Kingdom my
AppleLocale is en_GB, and my phone number is formatted to the
current locale.
You can also retrieve the phone number directly from the global
preferences:
NSString *phoneNumber = [[NSUserDefaults standardUserDefaults]
objectForKey:@"SBFormattedPhoneNumber"];
NSLog(@"Phone number: %@", phoneNumber);
You should be aware that the phone number might not always be
available unless the number has been set in the Phone→My Number preference panel in the Settings
application on the device. This is not guaranteed to be the case for all
devices, as some carriers don’t set this automatically.