Search
Follow
Recent Comments

Entries in settings (3)

Tuesday
Nov012011

Settings Radio Group Element

Whilst playing with the iCloud key-value store I came across a new element for the iOS settings application. The Radio Group element was introduced in iOS 5 and is similar in functionality to the Multi Value element. Most of the iOS documentation does not yet mention the Radio Group element with the exception of the Settings Application Schema Reference. Also Xcode 4.2 does not provide direct support when editing the settings property list which makes it a pain to add.

Both elements allow the user to choose one option from a list of possible options but the Radio Group element avoids the need to display the choices on a second page. To illustrate the difference I will add both Multi Value and Radio Group elements to the settings bundle I used to illustrate how to add a settings bundle to an iPhone app. Refer back to that earlier post for the basics on setting up user preferences.

Multi Value Element

The visual appearance of the Multi Value element is shown below for the Retry preference:

The preference is displayed as a singe line which when tapped takes the user to a second page where the available options can be selected:

The equivalent entry in the settings property list is as follows:

The required fields are as follows:

  • Type: PSMultiValueSpecifier
  • Title: This is the string which is displayed in the settings table.
  • Key: A string identifier which is used to set and retrieve the preference.
  • DefaultValue: Default value for the setting.
  • Values: An array containing the values the user must choose from. You can use any type supported by the preference file. Each value much have a corresponding title in the Titles array.
  • Titles: An array of strings which is displayed on the second page corresponding to the values defined in the Values array.

You can also add a ShortTitles array which provides a shorter string to show in the preference row. Notice in the previous screenshots how the first page show the current value as “Always” whereas the corresponding value on the second page has the longer title “Always Retry”.

Radio Group Element

The Radio Group element removes the need to have a second page and shows all of the options on the initial preferences page as shown below for the Sync preference:

The entry in the settings property list is as follows:

At the time I am writing this post I am using Xcode 4.2 which does not show the Radio Group as a valid option in an iPhone settings list. This has the side effect that the Item line incorrectly shows the values from the previous item in the settings list. However this does not impact the values written to the settings file (right-click on the file and use Open As > Source Code to see the actual XML).

The fields are similar to the Multi Value element except that the title is now optional since it serves as the title of the table section. Also you can add a FooterText string to include some extra explanatory text below the table group.

Setting and Accessing Values

Both preference settings can be accessed in a similar way via the NSUserDefaults class:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

NSInteger syncType = [defaults integerForKey:@”syncType”];

NSInteger retry = [defaults integerForKey:@”retryStrategy”];

Wrapping Up

As long as you do not have a huge list of settings I think the Radio Group element is a nicer choice for selecting from a set of options. It avoids the extra page and the title and footnote help make the settings easier to understand. I guess I should file some bug reports on Xcode and the documentation to ask Apple to update them now that the dust is settling from all the recent launch activity.

Wednesday
May192010

Managing User Preferences within an iPhone App

So yesterday I showed an example of using NSUserDefaults to add user preferences to an application. Today I will look at how to manage preferences from within the app.

Application Settings

The decision on whether to manage your applications settings and user preferences from within the application or in the separate iPhone Settings application depends a lot on when and how often you expect the user to want to make changes. Apple provides some guidance on making this decision:

Preferences that are integral to using the application (and simple enough to implement directly) should be presented directly by your application using a custom interface.

Preferences that are not integral, or that require a relatively complex interface, should be presented using the system’s Settings application.

One thing that you do want to avoid is using both methods in the same App. If you have a settings page within the application do not also include a settings bundle unless you want to confuse the user.

If you decide to implement a settings page within the application you will need to provide the user interface. Unless you have a really complicated application the user interface can be very simple and generally a table view controller will do the job nicely. Since I assume you know how to add a table view I will focus this post on storing and retrieving the user preferences.

Less is More

Before getting into the code I want to mention one more consideration about user preferences. It is tempting to add lots of options and settings to an application but you should think carefully about each one you add. Settings that you might find in a desktop application are not necessarily suitable for an iPhone application. It is worth asking what the impact on the user will be if you omit a setting and choose a sensible default. There is something about the simplicity of the iPhone UI that makes me feel that the user should be able to use the App without first having to read a User Guide or play with lots of options and settings.

Preferences Manager

I like to move all the code related to managing a users preferences into a single class which is then initialised as a shared instance when the application starts. The class is very simple in that it just contains a dictionary to hold the users preferences and a file path used when reading/writing the preferences file:

// Preference.h
@interface Preference : NSObject {

  NSMutableDictionary *prefsDict;
  NSString *prefsFilePath;
}

 

@property (nonatomic,retain) NSMutableDictionary *prefsDict;

@property (nonatomic,retain) NSString *prefsFilePath;

 

At this point I have only two methods for the class, one to get a reference to the shared instance and a second we will use to force the preferences to be saved to disk:

+ (Preference *)sharedInstance;
- (void)savePrefs;

 

Creating a Shared Instance

The implementation of the Preference class starts with the standard setup to synthesize the properties defined in the header file:

// Preference.m
#import "Preference.h"

@implementation Preference

@synthesize prefsDict;
@synthesize prefsFilePath;
// ...
@end

 

To produce a shared instance of the Preference class that we can use throughout the application we define a static reference to an instance of the class. A class method called sharedInstance is responsible for returning a reference to the shared instance if it is already declared or allocating and initialising the shared instance the first time it is accessed:

static Preference *sharedInstance = nil;

+ (Preference *)sharedInstance {

    if (sharedInstance == nil) {
        sharedInstance = [[self alloc] init];
    }
    return sharedInstance;
}

 

The first time the application calls the sharedInstance class method an instance is allocated and initialised. The init method for the class is pretty simple:

-(id)init {
	
    if (self = [super init]) {

        // Load or init the preferences
        [self loadPrefs];
    }
	
    return self;
}

Since this is a shared instance that we expect to live for the life of the application it will not normally get released until the application exits but we will include a dealloc for the instance just in case (the savePrefs method will be shown later):

- (void)dealloc {
	
    // Ensure the prefs are saved
    [self savePrefs];
	
    // release resources
    [prefsDict release];
    [prefsFilePath release];
    [super dealloc];
}

 

Now with the basic setup complete we need to fill in some of the methods. The first one we will cover is the method to load the preferences from disk when the object is initialised:

// Load the prefs from file, if the file does not exist it is created
// and some defaults set
- (void)loadPrefs {
	
    // If the preferences file path is not yet set, ensure it is initialised
    if (prefsFilePath == nil) {
        [self initPrefsFilePath];
    }
	
    // If the preferences file exists, then load it
    if ([[NSFileManager defaultManager] fileExistsAtPath:prefsFilePath]) {
        self.prefsDict = [[NSMutableDictionary alloc] initWithContentsOfFile:prefsFilePath];
    } else {
        // Initialise a new dictionary
        self.prefsDict = [[NSMutableDictionary alloc] init];
    }

    // Ensure defaults are set
    [self setDefaults];
}

The user preferences are stored in an NSMutableDictionary which can be very easily stored and read from an XML file using the writeToFile and initWithContentsOfFile methods. The path to the dictionary file is stored in the prefsFilePath instance variable which if it is not set is initialised by the initPrefsFilePath method which I will show shortly. Once we have the file path a check is made to see if the preferences file exists. If the file exists an NSMutableDictionary is allocated and initialised with the contents of the file. Otherwise an empty dictionary is created. Finally we set defaults for the applications preferences with the setDefaults method.

The preferences file is stored in the application documents directory. The method initPrefsFilePath constructs the full pathname to this file as follows:

- (void)initPrefsFilePath {
	
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSString *documentsDirectory = [paths objectAtIndex:0];

self.prefsFilePath = [documentsDirectory stringByAppendingPathComponent:@"appPrefs.xml"];

}

 

Saving the preferences back to disk is trivial:

- (void)savePrefs {

    [prefsDict writeToFile:prefsFilePath atomically:YES];

}

So far everything has been very generic but now things start to get App specific. I will stick to the example from yesterday and keep it simple with a single boolean user preference to enable/disable rotation. First we will define a string that we will use a dictionary key for storing and retrieving the preference:

NSString *kPref_rotation = @"enableRotation";

We now define methods to set and get this preference:

- (BOOL)enableRotation {

    NSNumber *enabled = [prefsDict objectForKey:kPref_rotation];
    return [enabled boolValue];
}

- (void)setEnableRotation:(BOOL)enabled {
	
    [prefsDict setObject:[NSNumber numberWithBool:enabled] forKey:kPref_rotation];
}

Note that a boolean value has to be first converted to an NSNumber object before it can be stored in the dictionary. The final method we are missing is the method to set defaults when we first initialised the Preference object:

- (void)setDefaults {

    if ([prefsDict objectForKey:kPref_rotation] == nil) {
        [prefsDict setObject:[NSNumber numberWithBool:YES] forKey:kPref_rotation];
    }	
}

Putting it all together

So after all of that we can finally look at how to implement the example from yesterdays post on how to access the rotation preference in our view controller:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {

    // Check user preferences
    Preference *appPrefs = [Preference sharedInstance];
	
    BOOL enabled = [appPrefs enableRotation];

    if (enabled) {
        return YES;
    } else {
        return (interfaceOrientation == UIInterfaceOrientationPortrait);
    }
}
Tuesday
May182010

Adding a settings bundle to an iPhone App

An example of how to add a simple settings bundle to an application to allow the user to set some application defaults. In this example we will add a switch to allow the user to lock the device rotation.

Adding a Rotation Lock

The ability of an application to rotate to match the orientation of the device you are holding is a great feature and Apple encourages App developers to support all possible orientations. However there are some situations such as when you are using the device lying down when the rotation can be annoying. The iPad is the first, but probably not the last, device that Apple has introduced that allows the user to lock the orientation.

Adding a software based rotation lock to an existing App for pre-iPad devices is not that difficult. You can basically break the problem down into two steps:

  • Allowing the user to specify their preference as to whether rotation is allowed in an app.
  • Enabling/disabling rotation in the view controllers

Setting the User Preference

There are two basic ways to allow a user to specify a user preference. The simplest way is to use NSUserDefaults to store the preferences in the standard iPhone Settings Application. Alternatively we can add a settings screen directly to the App and store the settings in a plist file within our own application bundle. To keep this example simple we will take the first option.

To get started we need to add a Settings Bundle to our Xcode project. Right-click on the top level folder in your project and select Add->New File from the menu. Select the Settings Bundle template found under Resources to add it to your bundle leaving the default name of Settings.bundle.

Expand the Settings.bundle to display the Root.plist file which by default will contain an Array named PreferenceSpecifiers containing four dictionary items. To make it easier to edit this file you can inform Xcode that it is an iPhone settings bundle. Select the Root.plist file, click on the Root entry in the detail view to ensure it is selected and then from the Xcode view menu select Property List Type -> iPhone Settings plist.

Delete the four items and add a new item that will allow us to enable/disable the rotation lock. The details of what we need to add are as follows:

Type (String): Toggle Switch - this specifies the PSToggleSwitchSpecifier which is a simple toggle switch than can be turned ON/OFF by the user.

Title (String): Rotation Lock - this is the name of the setting we want to display to the user.

Identifier (String): enableRotation - this is the key we will use to retrieve this setting in the application

Default Value (Boolean): YES - a default value for the key if not found in the preferences database (but see discussion below)

Value for ON: (Boolean): YES - the value the key should take when the switch is in the ON position. Make sure you set the type of this value to Boolean (right click on TrueValue and select Value Type -> Boolean).

Value for OFF: (Boolean): NO - the value the key should take when the switch is in the OFF position. Make sure you set the type of this value to Boolean.

If you did that correctly you should end up with something looking like this (make sure you set the types correctly especially for the Boolean fields).

If you save, build and then run the project in the simulator you should already see the entry for the application in the settings application as follows:

The icon displayed will be a scaled down version of the Icon.png file by default. If you want something different add an icon sized at 29 x 29 pixels named Icon-Settings.png to your application.

Even defaults need defaults…

One thing that is poorly explained in the Apple documentation is the use of the DefaultValue field. You would think that this sets the value that your application will see when your App runs for the first time. Unfortunately this is not the case. The default value is used to set the initial position of the toggle switch in the settings application. This is what the user will see when they open the settings application and look at the settings for the application:

However, it is important to understand that until the user actually changes the value of the setting nothing is actually set. If you check for the setting in your application it will actually return nil unless you set a default value. To do that add the following to the applicationDidFinishLaunching (or didFinishLaunchingWithOptions) method:

  // Set the application defaults
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  NSDictionary *appDefaults = [NSDictionary dictionaryWithObject:@"YES" forKey:@"enableRotation"];
  [defaults registerDefaults:appDefaults];
  [defaults synchronize];
 

Now if the user has not yet set a value it will default to YES. To check the value anywhere in the application takes just a couple of lines of code:

  // Get user preference
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  BOOL enabled = [defaults boolForKey:@"enableRotation"];

 

Putting it all together

So now that we have the user preferences setup we can check the value in any of our view controllers to determine whether to permit rotation or not:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {

  // Get user preference
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  BOOL enabled = [defaults boolForKey:@"enableRotation"];

  if (enabled) {

    return YES;

  } else {

    return (interfaceOrientation == UIInterfaceOrientationPortrait);

  }
}