Formatters and Locale Changes

I recently posted about some of the common ways of using number formatters. In doing so I omitted to mention that is common to cache both number and date formatters in iOS applications and a small issue this can cause if a user changes the device locale for a running app.

Caching Formatters

When you need a date or number formatter in a view controller it is often a good idea to cache the resulting object once you have created it. Consider for example a table view cell that uses both a number and date formatter:

cell format uk

It would be wasteful to create new formatters each time we need to layout a cell. A more efficient approach is to create them once in the view controller and cache the formatter objects in instance variables. You could do this in a method such as viewDidLoad but for reasons that I will explain shortly it can be more convenient to use a getter method to lazily instantiate each formatter.

I should note at this point that all of the following code snippets assume iOS 5 with ARC enabled. If you are targeting iOS 4 or earlier you will need to take care of memory management

Assuming that my table view controller class is named UYLMasterViewController I could add properties for the two formatters to a private class extension as follows (in UYLMasterViewController.m):

@interface UYLMasterViewController ()
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
@property (nonatomic, strong) NSNumberFormatter *numberFormatter;
@end

I like to place properties like this in the private class extension rather than in the public class interface as they represent internal implementation details which should remain private. It also has the benefit of keeping the interface (.h) file clear of clutter. The corresponding instance variables are synthesized in the class implementation:

@implementation UYLMasterViewController

@synthesize dateFormatter=_dateFormatter;
@synthesize numberFormatter=_numberFormatter;

This creates a default getter and setter for each ivar but we will create our own getter methods to allow the formatters to be configured the way we want them. The getter method for the date formatter which will show just the month and year could be written as follows:

- (NSDateFormatter *)dateFormatter
{
  if (_dateFormatter == nil)
  {
    _dateFormatter = [[NSDateFormatter alloc] init];
    [_dateFormatter setDateFormat:@"MMMM yyyy"];
  }
  return _dateFormatter;
}

If the formatter does not already exist it will be created and stored in the ivar otherwise the existing object is returned. Likewise to format the number with locale specific grouping and decimal separators we could write the getter for the number formatter as follows:

- (NSNumberFormatter *)numberFormatter
{
  if (_numberFormatter == nil)
  {
    _numberFormatter = [[NSNumberFormatter alloc] init];
    [_numberFormatter setPositiveFormat:@"#,##0.00"];
  }
  return _numberFormatter;
}

The first time the code to format the table view cell accesses the formatters via the getter method they will be allocated and cached in the instance variables. Note that since we are using ARC we do not need to worry about releasing these objects when the view controller is deallocated.

The table view data source method to return the correctly formatted table cell would like something like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  NSNumber *value = [NSNumber numberWithInteger:(indexPath.row+1)*1000];
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
  cell.textLabel.text = [self.numberFormatter stringFromNumber:value];
  cell.detailTextLabel.text = [self.dateFormatter stringFromDate:[NSDate date]];
  return cell;
}

Note that to keep this example code simple I am generating the data that is displayed from the cell row number and the current date. The actual cell layout is defined using a prototype table cell and storyboard.

Locale Changes

There is one issue with caching the date and number formatters in this way. The formatters depend on the current user locale when they are created. However if the locale changes the cached formatters will not get updated. This means that if the user changes the locale whilst the app is running, or more likely for an iOS application, whilst it is suspended in the background the number and date formatters will no longer reflect the users locale.

For example, the screenshot of the table view I showed earlier was from the iOS Simulator running with a United Kingdom region format. If I change the region format (Settings > General > International > Region Format) to French-France I might expect to see the following:

cell format fr

Unfortunately as currently implemented the only way the table view will reflect the changed locale is if I force the application to restart. You might not consider this to be a huge problem, after all how often does a user change the locale of their device? On the other hand it takes just a few lines of code to correct and will avoid surprising/annoying a user who does change the locale whilst your app is sleeping peacefully in the background.

You can easily detect that the user has change the locale by registering an observer for the NSCurrentLocaleDidChangeNotification notification. A good place to do that is in the viewDidLoad method of the view controller:

- (void)viewDidLoad
{
  [[NSNotificationCenter defaultCenter] addObserver:self
                      selector:@selector(localeDidChange)
                          name:NSCurrentLocaleDidChangeNotification 
                        object:nil];
}

We should also ensure that we remove the observer when the view controller is deallocated. In this simple case since we are using ARC this is the only action in our dealloc method:

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

When our view controller receives the notification the localeDidChange method that we registered can simply remove the number and date formatters by setting them to nil and request the table view to reload:

- (void)localeDidChange
{
  self.numberFormatter = nil;
  self.dateFormatter = nil;
  [self.tableView reloadData];
}

Since we use lazy initialisation to create the formatters they will then be recreated with the new locale as part of the table view being reloaded. The user will then immediately see the number and date correctly formatted in the current locale without having to restart the app.