Benjohn Benjohn - 4 months ago 30
iOS Question

Using setDoesRelativeDateFormatting: YES and setDateFormat: with NSDateFormatter

NSDateFormatter has a lovely support for producing relative dates such as "Today", "Tomorrow", "Yesterday" when these are supported by the current language. A great advantage is that all of these are localised for you already – you don't need to translate the strings.

You can turn on this functionality with:

[dateFormatter setDoesRelativeDateFormatting: YES];


The down side is that this only seems to work for an instance that is using one of the predefined formats, such as:

[dateFormatter setDateStyle: NSDateFormatterShortStyle];


If you set up the date formatter to use a custom format, like this:

[dateFormatter setDateStyle: @"EEEE"];


Then when you call:

[dateFormatter stringFromDate: date];


… you will just get back an empty string.

I'd like to be able to get relative strings when possible, and use my own custom format when it is not.

Answer

I set up a category on NSDateFormatter to provide a workaround for this. An explanation follows the code.

In NSDateFormatter+RelativeDateFormat.h:

@interface NSDateFormatter (RelativeDateFormat)
-(NSString*) relativeStringFromDateIfPossible:(NSDate *)date;
@end

In NSDateFormatter+RelativeDateFormat.m:

@implementation NSDateFormatter (RelativeDateFormat)
-(NSString*) relativeStringFromDateIfPossible:(NSDate *)date
{
  static NSDateFormatter *relativeFormatter;
  static NSDateFormatter *absoluteFormatter;

  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    const NSDateFormatterStyle arbitraryStyle = NSDateFormatterShortStyle;

    relativeFormatter = [[NSDateFormatter alloc] init];
    [relativeFormatter setDateStyle: arbitraryStyle];
    [relativeFormatter setTimeStyle: NSDateFormatterNoStyle];
    [relativeFormatter setDoesRelativeDateFormatting: YES];

    absoluteFormatter = [[NSDateFormatter alloc] init];
    [absoluteFormatter setDateStyle: arbitraryStyle];
    [absoluteFormatter setTimeStyle: NSDateFormatterNoStyle];
    [absoluteFormatter setDoesRelativeDateFormatting: NO];
  });

  NSLocale *const locale = [self locale];
  if([relativeFormatter locale] != locale)
  {
    [relativeFormatter setLocale: locale];
    [absoluteFormatter setLocale: locale];
  }

  NSCalendar *const calendar = [self calendar];
  if([relativeFormatter calendar] != calendar)
  {
    [relativeFormatter setCalendar: calendar];
    [absoluteFormatter setCalendar: calendar];
  }

  NSString *const maybeRelativeDateString = [relativeFormatter stringFromDate: date];
  const BOOL isRelativeDateString = ![maybeRelativeDateString isEqualToString: [absoluteFormatter stringFromDate: date]];

  if(isRelativeDateString)
  {
    return maybeRelativeDateString;
  }
  else
  {
    return [self stringFromDate: date];
  }
}
@end

How does this work?

It maintains two date formatters using an (arbitrary) standard format. They format identically except that one will provide relative date strings and the other will not.

By formatting a given date with both formatters, it is possible to see if the relative date formatter is giving a special relative date. There is a special relative date when the two formatters give different results.

  • If there is a special relative date string, that special relative date string is returned.
  • If there is not a special relative date string, the original formatter is used as a fallback and the format settings you have defined on it are used.

You can find out more about dispatch_once here.

A Limitation…

The implementation does not handle time components that you may have placed in to your format string. When a relative date string is available, your format sting is ignored and you get the relative date string.