Ranjit
Ranjit

Reputation: 4636

Keep NSDates time zone independent

I am working with an app which has a calendar in it, I am adding few notes to particular dates, and saving dates and notes to plist, so when I start my app I retrieve date from plist and show it on calendar date with a color mark, to indentify that the date has a note.

When user selects a date to add notes I create the date this way

 NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];

    NSDateComponents *weekdayComponents = [gregorian components:(NSDayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit  | NSMinuteCalendarUnit)fromDate:**date selected**];
    NSInteger day    = [weekdayComponents day];
    NSInteger month  = [weekdayComponents month]; 
    NSInteger year   = [weekdayComponents year];

    NSDateComponents *timeZoneComps=[[NSDateComponents alloc] init];
    [timeZoneComps setDay:day];
    [timeZoneComps setMonth:month];
    [timeZoneComps setYear:year];
    [timeZoneComps setHour:00];
    [timeZoneComps setMinute:00];
    [timeZoneComps setSecond:01];    


    NSDate *date = [gregorian dateFromComponents:timeZoneComps];

and save this date to plist

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

    NSString *filePath = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"file.plist"];

    NSMutableDictionary *d = [NSMutableDictionary new];
    [d setObject:date forKey:@"my-date"];

    [d writeToFile:filePath atomically:YES];

Now when I open the app I iterate through my calendar dates and make comparison to mark a not on calendar

here is this

if([[**dateFromPlist** isEqualToDate:**dateFromCalendar**])
{            
        do something
}

This works when I write and read the date in one time zone, But if I write the date , suppose I create a note on indian time zone (device) on 10 -jul -2014 and then I change time zone to US (device) and open my app, the if condition never executes because it gets different time.

Please help me in finding out , where I am going wrong.

Regards Ranjit.

Upvotes: 1

Views: 533

Answers (1)

uchuugaka
uchuugaka

Reputation: 12782

NSDate has no timezone. It's a wrapper around unix time, a floating point number of seconds since the reference date. (A fixed point in time) NSTimeInterval is a typedef for a double and is the type used to represent this floating point value.

What you should be doing is storing the NSTimeInterval value. Then create NSDate objects from that value when you read it from the file.

Timezone is strictly part of the presentation to users.

Assuming someDate is an NSDate

    NSDate *someDate = [NSDate date];
    NSTimeInterval someDateAsInterval = someDate.timeIntervalSinceReferenceDate;

Ok, now you have your double, a.k.a. NSTimeInterval. Let's save that to a plist and then read it back. We are going to use NSDictionary and NSNumber literal syntax. (I don't need a mutable dictionary here.) NSNumber is a great way to store scalars in plists.

    NSString *aFilePath = [@"~/Documents/my_fancy_file.plist" stringByExpandingTildeInPath];

    NSDictionary *dateDict = @{@"dateAsInterval": @(someDateAsInterval)};
    [dateDict writeToFile:aFilePath atomically:YES];

    NSDictionary *dateDictFromFile = [NSDictionary dictionaryWithContentsOfFile:aFilePath];
    NSTimeInterval dateFromFileAsTimeInterval = [dateDictFromFile[@"dateAsInterval"] doubleValue];
    NSDate *dateFromFile = [NSDate dateWithTimeIntervalSinceReferenceDate:dateFromFileAsTimeInterval];
    NSLog(@"\n someDate\n%@\n dateFromFile\n%@", someDate, dateFromFile);

The last bit below is just to show how convenient it is. Oh did I mention that NSTimeInterval could be a lot cheaper than a full NSDate object? That's a nice plus.

    if ([someDate isEqual:dateFromFile]) {
        NSLog(@"SAME");
    }

    if (someDateAsInterval == dateFromFileAsTimeInterval) {
        NSLog(@" == ");
    }

Now there is one more thing you're totally screwing up, but you're not alone and it's not obvious or easy. You can learn a lot of the following from the Date & Time Programming Guide in the docs. The rest is well covered in WWDC videos. In fact it is something that people screw up so much it is covered almost every year at WWDC. This year was no exception. This is going to be easier to understand when you recall that NSDate is actually an object wrapping a typedef'd double, NSTimeInterval. This provides

Let's quote the docs:

NSDate provides methods for creating dates, comparing dates, and computing intervals. Date objects are immutable. The standard unit of time for date objects is floating point value typed as NSTimeInterval and is expressed in seconds. This type makes possible a wide and fine-grained range of date and time values, giving precision within milliseconds for dates 10,000 years apart.

That means the object gives you methods instead of C style functions. A lovely Objective-C world. The backing data structure, NSTimeInterval gives you incredible precision.

There is one more thing you need to know that is discussed in the recent WWDC videos.

Comparing dates and Calendrical calculation is hard for reasons that might surprise you. The biggest one to think about is that not all dates have a midnight and some have more than one! So comparing two dates at midnight can be a mess. It's complicated but real. Don't do it. Best practice: use a time during the day, closer to noon. All Dates have that time and only have it once. Wait, why am I talking about time? Look back and think carefully. NSDate by name misleads a lot of people, and we insert a lot of assumptions about something so common we think it is simple, but is deceptively complex in fact. For convenience we tend to think of dates as days on a calendar, but in fact, dates are and NSDate is a specific point in time.

It turns out that it is a common mistake to use the NSCalendar method dateFromComponents: without including the finer components. The sad part is it gives you a default value that is 00:00:00 for the time if you do not supply the time, and that results in unreliable dates for comparisons and calculations.

But I digress. The key thing here is to use your NSTimeInterval value. That is easy to compare. Easy to store. Hard to get wrong. You can create an NSDate from it as needed. You can display that with the current locale as needed.

Here is what the plist content looks like.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>dateAsInterval</key>
    <real>426751774.87438202</real>
</dict>
</plist>

Upvotes: 4

Related Questions