Just use NSDate
, right?
The obvious choice for handling dates in iOS and OS X apps is NSDate
. It’s obvious, right? It’s got “date” right in its name, doesn’t it?
And that’s the thing about NSDate
. It’s mis-named. It almost says so right in the docs:
NSDate
objects encapsulate a single point in time, independent of any particular calendrical system or time zone.
A single point in time? I don’t know about you but I think of “date” as meaning more like
[T]he day of the month or year as specified by a number: what’s the date today? | please give your name, address, and date of birth. [source: OS X’s dictionary]
And there you have it. NSDate does not represent a date, at least not as the word “date” is commonly understood. So what, basically, the hell is it?
That line from the docs pretty much says it, though the implications might not be obvious. An NSDate
is a single instant in time, down to the fraction of a second. The class is an object interface to a single floating point number representing the number of seconds since a reference point in time. Why it’s called NSDate
is a question to which I have no answer.
Saying it’s independent of any calendrical system is a fancy way of saying, basically, this is not actually a date in any rational sense of the word. NSDate
won’t help you find out the date associated with its value. You can use NSDateFormatter
to get a human-readable date, but often that’s still not really what you need.
Time Zones Rear their Ugly Head
Mentioning that NSDate
is independent of a time zone is a key point of understanding. People often make the mistake of wondering what time zone an NSDate
is in, or worse, how to convert an NSDate
from one time zone to another. It has no time zone. If two people in different time zones call NSDate()
at exactly the same moment, they’ll get exactly the same result.
Zones come in because they’re a necessary part of getting from an NSDate
to an actual date. As of just a moment ago, NSDate().timeIntervalSinceReferenceDate
gives me a floating point value of 477959641.90769. So what date is that? Where I’m sitting, it’s 23 February, 2016. If I were sitting in Singapore, I’d get the same value from NSDate
but the date would be 24 February, 2016. So what date does that NSDate
represent? It depends what time zone you’re in.
That’s fine as far as it goes, and it’s more than sufficient for a lot of things. If you just want to know when something happened, NSDate
is there for you. If you want to know if thing A happened before or after thing B, NSDate
has got your back. But what if you want to know everything that happened on 23 February 2016?
Now all of a sudden NSDate
isn’t looking so useful. Strictly speaking, you’ll have to do something like
- Pick a time zone that seems appropriate
- Work out the
NSDate
corresponding to the beginning of 23 Feb in that time zone - Work out the
NSDate
corresponding to the end of 23 Feb in that time zone - Filter your data using these two floating point values.
None of this is impossible but it’s more complex than it feels like it should be. It can also lead to unexpected results. Maybe I’m using a journalling app, and I create a couple of entries on 23 Feb. Then I fly to Singapore. And now the app shows that those entries were created on 24 Feb while none were created on the 23rd, because the creation date is an NSDate
. Except I know I didn’t create those entries on the 24th because I was writing on the 23rd about an event that happened that day. Your app is now strictly correct but, from my perspective, wrong. But hey, NSDate
, right? Whaddya gonna do?
It gets worse. What if you need to group your data by the week of the year? You’d have to repeat the steps above for every possible week covered by the data.
Store data that you actually need
Cases like these are why it’s important to make sure you’re storing the data you’ll actually need later on instead of just adding an NSDate
and thinking you’re done.
What that is depends on how you think you’ll need to use the data. If you’ll be searching, filtering, or grouping your data by date components, include fields to represent those components in your data model. If it’s a date that should remain fixed, you probably want day, month, and year. If you’re grouping by week, store the week of the year as its own field. Anything from NSDateComponents
is a likely choice depending on what your app does. You probably don’t want NSDateComponents
itself though. It conforms to NSCoding
for easy archiving, of course. But once archived you can’t use it for searching, etc, without de-archiving first.
A trivial journal-entry model struct might look like this:
struct JournalEntry {
var text: String
var year: Int
var month: Int
var day: Int
init(text: String) {
self.text = text
let dateComponents = NSCalendar.currentCalendar().components([.Year, .Month, .Day], fromDate: NSDate())
self.year = dateComponents.year
self.month = dateComponents.month
self.day = dateComponents.day
}
}
If the only important consideration is displaying a consistent date to the user, you might be fine using NSDateFormatter
to create a string representation of the date, and saving just that string. You can’t easily sort or filter data with that, but if you don’t care about those things then don’t save data for them. In that case the trivial journal entry might be represented as
struct JournalEntry {
var text: String
var dateString: String
init(text: String) {
self.text = text
let formatter = NSDateFormatter()
formatter.dateStyle = .LongStyle
formatter.timeStyle = .NoStyle
self.dateString = formatter.stringFromDate(NSDate())
}
}
You might still want to include an NSDate
. They’re handy for sorting objects quickly, since ordering only considers a single numeric value. But if you need something else, be sure to include that as well.
Core Data vs. Dates
All of the above applies when using Core Data as well. The only detail to be aware of is that Core Data’s Date
type just means you’re reading and writing NSDate
. If NSDate
isn’t quite what you need, neither is Date
.