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:

@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:

#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);
  }
}