Separate date and time components in NSDate

April 07, 2007

Cocoa makes it easy to bind an NSDate object to an NSTextField, allowing the user to view a date and time without writing any code for formatting or validation. As is often the case though, this doesn’t always exactly fit with what we want to achieve. There are situations where the user might want to change the date or the time, but not both; when you design your UI, the best choice would be to use two text field controls, one for the time, and one for the date.

You can bind two NSTextFields to a single NSDate, and by carefully tweaking the NSDateFormatters you can make one display only the time, and the other display the date. It’s not quite that simple though; when you change the date, it also changes the time to 12:00, even though it’s not shown in the control. As a quick fix you could just add a second NSDate to your class, but that’s just being lazy. Don’t be lazy!

One of the big secrets to Cocoa bindings is that when you bind something to a value, you don’t actually have to bind it to a value. Confused? Remember, when you bind a control to an object in Interface Builder (let’s say, bind a text field to the variable someDate) you’re not actually binding it directly to that object, you’re binding it to the KVO compliant accessor / mutator that provide access to the variable. This provides a perfect place to manipulate the object in question, and that’s just what we’re going to do.

Let’s assume we have a simple class that holds an NSDate instance variable named “start”. We’re going to add two new accessor and mutator methods, one for the time and one for the date.

- (NSDate *)startTime;
{
    return [self start];
}

- (NSDate *)startDate;
{
    return [self start];
}

- (void)setStartTime:(NSDate *)date;
{
    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDateComponents *dateComponents = [calendar components:( NSYearCalendarUnit | NSMonthCalendarUnit |  NSDayCalendarUnit ) fromDate:[self start]];
    NSDateComponents *timeComponents = [calendar components:( NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit ) fromDate:date];

    [dateComponents setHour:[timeComponents hour]];
    [dateComponents setMinute:[timeComponents minute]];
    [dateComponents setSecond:[timeComponents second]];

    [self willChangeValueForKey:@"start"];
    NSDate *newDate = [calendar dateFromComponents:dateComponents];
    [self setStart:newDate];
    [self didChangeValueForKey:@"start"];
}

- (void)setStartDate:(NSDate *)date;
{
    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDateComponents *timeComponents = [calendar components:( NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit ) fromDate:[self start]];
    NSDateComponents *dateComponents = [calendar components:( NSYearCalendarUnit | NSMonthCalendarUnit |  NSDayCalendarUnit ) fromDate:date];

    [timeComponents setYear:[dateComponents year]];
    [timeComponents setMonth:[dateComponents month]];
    [timeComponents setDay:[dateComponents day]];

    [self willChangeValueForKey:@"start"];
    NSDate *newDate = [calendar dateFromComponents:timeComponents];
    [self setStart:newDate];
    [self didChangeValueForKey:@"start"];
}

This looks a little complicated at first, but it’s actually very simple. The accessor methods startTime and startDate just return the start instance variable. Since NSDateFormatter knows how to show only the date or time parts of an NSDate (once you configure it in Interface Builder), it doesn’t matter that we’re providing it with both.

The mutator methods are where things get a little tricky. We’ll need to use the NSDateComponents class, which lets us pick and choose where each time and date component comes from; either the original NSDate, or the new NSDate from the NSTextField. We use the NSCalendar class to get these components, to ensure things like local daylight savings time values are taken into account.

Once we build the correct NSDate, it’s simple to update our class variable. Just make sure to call willChangeValueForKey and didChangeValueForKey, so any bound objects will be updated.

You can put these methods in the object itself, or if you really believe in the MVC way of doing things you can just make a category for them to completely separate the data from the interface.

Marc Charbonneau is a mobile software engineer in Portland, OR. Want to reply to this article? Get in touch on Twitter @mbcharbonneau.