Countdown to a point in time using NSDate and NSDateFormatter

Last year we wrote an app for a summer camp - Camp Morasha - for the iPad, iPhone and iPod touch. Part of the app had a large countdown to when camp started. Programming a countdown in iOS is not particularly difficult but it does have a few challenges.

In the UIViewController that contains the countdown view I create an NSTimer that will call a function every second. If you want a faster countdown - for a chess clock for example - I recommend every 1/10 of a second, as that is about how fast the human eye can perceive things. There are outlets to UILabels for each of the Date Components; one for day, one for hour, one for minutes and one for seconds.They are named "counDown_[timeUNIT]". There is also an outlet to the UIView containing the countdown call countDownHeaderView. There is another view which appears in the same area as the countdown view but start hidden. This UIView named countDownOverHeaderView will only be displayed once the countdown is over.


-(void) countDown:(NSTimer*)theTimer
{
	unsigned int unitFlags = NSSecondCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSDayCalendarUnit;
	//self.campStartDate is lazily created
	NSDate* countDownDate=self.campStartDate;

	if ([self.campStartDate timeIntervalSinceNow] <0 ) //If count down is over stop timer and hide countdown - show UIView with generic message
    {
		countDownHeaderView.hidden=YES;
		countDownOverHeaderView.hidden=NO;
		[theTimer invalidate];
	}
    //self.gregorian is lazily created
	NSDateComponents *comps = [self.gregorian components:unitFlags fromDate:[NSDate date]  toDate:countDownDate  options:0];

	countDown_days.text=[NSString stringWithFormat:@"%d", [comps day]];
	countDown_hours.text=[NSString stringWithFormat:@"%d", [comps hour]];
	countDown_minutes.text=[NSString stringWithFormat:@"%d", [comps minute]];
	countDown_seconds.text=[NSString stringWithFormat:@"%d", [comps second]];
}

And important thing to understand in this code is that there are two instance variables that are created only once - so they are not created, and recreated every second; the calendar and the date. In general is not a big deal to create a NSCalendar that is only used for one function, but you should avoid that if you going to call the function hundreds of times.
The creation of the NSCalendar is very simple:

-(NSCalendar*) gregorian
{
	if (!gregorian)
    {
		gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
	}
	return gregorian;
}

Following the standard pattern of lazy substantiation.

The NSDate is a bit more complex, because I used an NSDateFormatter to create it:

-(NSDate*) campStartDate
{
	if (!campStartDate)
    {
		NSDateFormatter* dateFormatter=[[[NSDateFormatter alloc] init] autorelease];
		NSLocale *usLocale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease];
		[dateFormatter setLocale:usLocale];
		[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"EST"]];
		[dateFormatter setDateFormat:@"yyyy-MM-dd 'at' HH:mm"];
		campStartDate=[dateFormatter dateFromString:@"2011-06-27 at 9:30"];
		[campStartDate retain];
	}
	return campStartDate;
}

Something important to note is that the timezone must be set, since this is an absolute point in time, and not a relative point in time. If it was a countdown to new year's, a phone in Japan should have a lower number than the phone in Hawaii. But here, every phone in the entire world should have the exact same count.

Looking back I should I just calculated the unix time and them put that into the app - as that is a lot less finicky than NSDateFormatter which can interpret stings differently in different locations:

-(NSDate*) campStartDate{
    if (!campStartDate) {
        //1309224600 is unix time for 2011-06-27 at 9:30 est
        //http://www.wolframalpha.com/input/?i=2011-06-27+at+9%3A30+am+est+in+unix+time
        campStartDate=[[NSDate alloc] initWithTimeIntervalSince1970:1309181400];
    }
    return campStartDate;
}

The clear advantage is that there is only one line to debug, and absolutely no complication from NSLocale or NSTimeZone. The problem is that a unix time stamp is hard to a human to confirm is the correct time. That is why the comment, and the link to confirm is %100 a requirement. Another option is to use the NSDateFormatter to get the Unix time, NSLog it, and then use that.

The general rule is that NSDateFormatter is designed to work differently on different phone - which is really cool if you want to display different personalized string depending on the setting of the phone. But it is a big pain if you are relying on a particular behavior from NSDateFormatter. You need to make sure that the timezone is set, and possibly the locale and calendar as well. Computers want to tell time in UNIX time not string - so we should work with them and create NSDates in the format they want whenever possible.

Submit a Comment