As an active contributor on Stack Overflow, primarily in the iOS, Swift, and Objective-C tags, I see a lot of questions related to dates, date formatting, and date manipulations. This article's goal is to help clarify many of the misunderstandings and mistakes made by developers new to working with dates in Swift or Objective-C.
In Swift (3 and later) you will be working with
Date
,
Calendar
,
DateFormatter
,
DateComponents
,
Timezone
,
and Locale
.
In Objective-C you will be working with
NSDate
,
NSCalendar
,
NSDateFormatter
,
NSDateComponents
,
NSTimezone
,
and NSLocale
.
If you happen to run into some old Swift (2 or earlier), you will see the same classnames as Objective-C.
This article will be in Swift 3/4 and Objective-C and should work on any platform that supports the
Foundation
framework.
Let's start with the most basic class, Date
.
Date
represents a single point in time. It is important to understand that a Date
has no concept of timezone.
Think of Date
as saying "now". That Date
represents that specific moment. No matter where in the world you happen
to be, that "now" is the same "now" everywhere in the world. It happens at the same moment for
everyone. Of course people around the world will see a different time shown on their local clocks but that doesn't
change the moment it happened.
The only times you need to worry about a specific timezone is if you need to convert a string representation
of a date into a Date
or you wish to display a Date
to a user (or yourself when debugging code).
A common task is to get a Date
instance that represent "now". This is done as follows:
// Swift
let now = Date()
// Objective-C
NSDate *now = [NSDate date];
This seems simple enough but this little line of code is where many developers get confused. And that confusion
comes from looking at the output of this Date
instance. Let's add a line that prints now
. And let's
say you live in New York City and you run this code on March 1, 2018 at 2:42pm local time.
// Swift
print(now)
// Objective-C
NSLog(@"%@", now);
You will see the following output:
2018-03-01 19:42:00 +0000
Notice I stated you ran this code at 2:42pm. Why are we seeing the time as 19:42? And what is that +0000?
A quick detour.
When you print a Date
(or view a Date
in the debugger), the description
(or debugDescription
) function is called. In fact, the following two lines are effectively the same:
// Swift
print(now)
print(now.description)
// Objective-C
NSLog(@"%@", now);
NSLog(@"%@", now.description);
This returns a string representation of the date.
The current implementation of the description
method of Date
is to show the date using 24-hour time
in the UTC timezone. Remember above I stated that one of the few times you need to worry about timezones is when
displaying a Date
to a user? This is one of those times. It just so happens that the current implementation of
the description
method of Date
is to use the UTC timezone.
Back to the output. The +0000 is the timezone being used to represent the date. The 0000 is the hours and minutes offset from UTC time and the + means it is a positive offset. Since the chosen timezone is UTC, it has no offset from UTC, hence the offset of +0000. New York City is 5 hours behind UTC on March 1, 2018. 2:42pm in 24-hour time is 14:42. Take into account the 5 hour time difference between New York City and UTC time, and we get the output of 19:42 UTC time. 2:42 pm in New York is at the same moment as 19:42 UTC time.
Here's one more way to think about this. You are in New York City talking on the phone to a friend who is currently
in the UTC timezone. You are both looking at a clock. At 2:42pm local time in New York City you say "now" to your
friend on the phone. Your friend looks at their clock and sees that it says 19:42 (it's a 24-hour clock). You are
both experiencing the same "now". That is your Date
. Despite the two clocks showing two different
times, your "now" and your friend's "now" are the same moment. It's not until you print the Date
(look at your clocks) do you see a specific local time. Printing a Date
is the same as looking at
your friend's clock in the UTC timezone. It doesn't change the moment represented by the Date
. It
simply gives you a representation of that moment using a specific clock.
Certainly by now you are saying, "But this is confusing. I want to see the date in my own local timezone so it
matches my own clock". In addition to the description
property, Date
provides the
description(with:)
function. The parameter is a Locale
which I'll cover more later.
For now, simply pass the value .current
which means your current locale. By doing this, the date
is also formatted to use your current timezone.
The following code will print the date in your own timezone and locale. Use this if you don't want to see the date in UTC time.
// Swift
print(now.description(with: .current))
// Objective-C
NSLog(@"%@", [now descriptionWithLocale:NSLocale.currentLocale]);
You will see the following output (depending on your timezone and locale):
Thursday, March 1, 2018 at 2:42:00 PM Eastern Standard Time
In the DateFormatter
section you will learn how to display
Date
values in different formats.
The Locale
class represents a locale. According to the reference documentation:
Locale
encapsulates information about linguistic, cultural, and technological conventions and standards. Examples of information encapsulated by a locale include the symbol used for the decimal separator in numbers and the way dates are formatted.
When formatting a date, the locale defines how the result will look. This includes the language used for month and weekday names as well as the order the year, month, and day are displayed and what punctuation appears.
In most cases you will want to use the current user's locale and in most cases, date formatting code will use the current locale without having to explicitly choose it. If you need to get a reference to the current locale, you can write the following line of code:
// Swift
let locale = Locale.current
// Objective-C
NSLocale *locale = NSLocale.currentLocale;
There are times where you might want to use a specific locale. For this you need to know the locale's identifier.
In most cases the identifier is a combination of a language code and a country code. For example, in English
speaking parts of Canada you would use en_CA
while in French speaking portions you would use
fr_CA
. Refer to the documentation for more details on locale identifiers. To get a Locale
reference for a specific locale identifier, you can write code as follows:
// Swift
let locale = Locale(identifier: "en_CA")
// Objective-C
NSLocale *locale = [NSLocale localeWithLocaleIdentifier:@"en_CA"];
There is one very special locale that you may need to use in certain cases (which will be covered later). This
has the special identifier of en_US_POSIX
.
// Swift
let posixLocale = Locale(identifier: "en_US_POSIX")
// Objective-C
NSLocale *posixLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
The TimeZone
class represents a timezone. The only time you need to worry about timezones is when
converting between Date
and String
(in either direction). Most operations default
to the user's current timezone. The one notable exception is the description
method used when
printing a Date
which defaults to using the UTC timezone.
If you need to get a reference to the user's current timezone, you can use:
// Swift
let tz = TimeZone.current
// Objective-C
NSTimeZone *tz = NSTimeZone.systemTimeZone;
You can also get references to specific timezones using either the timezone's unique identifier, the timezone's non-unique abbreviation, or from the timezone's offset from UTC (in seconds). See the reference documentation for complete details.
I also suggest reading the Time zones section of the Coordinated Universal Time page on Wikipedia. This will help you better understand the relationship between UTC, Zulu (Z), and GMT.
Now that you have a better understanding of Date
, Locale
, and TimeZone
,
it is time to learn about date formatting. This
can either be converting a Date
into a String
or converting a String
into
a Date
.
One very common task is to display a date to a user. The proper way to do this is to use a
DateFormatter
.
A DateFormatter
lets you convert a Date
to a String
taking into account
the locale. This ensures the date is formatted properly. The names of months and weekdays appear in the
appropriate language and the order of the date's components (year, month, day, etc.) are correct and separated
with the normal punctuation the user is accustomed to.
A date formatter defaults to the current user's locale and the device's current timezone. If you wish to format
the date using a different locale and/or a different timezone, you can set the locale
and/or
timeZone
properties as needed.
DateFormatter
provides two approaches to converting a Date
to a String
:
styles and formats. The preferred method is to use styles. This ensures that the resulting output is in a format
that a user expects. Avoid using specific formats when showing a date to a user. For example, let's say you
format your date to a specific format and the result is:
8/3/18
Depending on where the user lives, this could be interpreted as August 3, 2018, March 8, 2018, or it would be totally unfamiliar. By using styles with the date formatter, the result will come out in the appropriate format for the user's locale and will avoid any confusion.
When you wish to display a date to a user, it is best to setup your date formatter with appropriate date and time
styles. This will provide a result most appropriate to the specified locale (which defaults to the user's locale).
If you only want the date and no time, set the timeStyle
property to .none
. Conversely,
if you only want the time and no date, set the dateStyle
property to .none
. For the
parts you do want, choose one of .short
, .medium
, .long
, or
.full
. The documentation for DateFormatter.Style
provides examples of how each of these
affect the date and time. But ultimately the result is dependent on the locale. Of course if you want both the
date and the time, you can use different styles for each. They do not need to be same.
Here is an example for setting up a formatter for a long date style and a medium time style:
// Swift
let dfs = DateFormatter()
dfs.dateStyle = .long
dfs.timeStyle = .medium
print(dfs.string(from: date))
// Objective-C
NSDateFormatter *dfs = [[NSDateFormatter alloc] init];
dfs.dateStyle = NSDateFormatterLongStyle;
dfs.timeStyle = NSDateFormatterMediumStyle;
NSLog(@"%@", [dfs stringFromDate:now]);
Here are some example results, followed by the locale, for a date representing March 1, 2018 at 2:42 pm:
March 1, 2018 at 2:42:00 PM en_US (English/USA) 1 March 2018 at 14:42:00 en_GB (English/Great Britain) 1. März 2018 um 14:42:00 de_DE (German/Germany) 1 mars 2018 à 14:42:00 fr_FR (French/France) ١ مارس، ٢٠١٨، ٢:٤٢:٠٠ م ar_EG (Arabic/Egypt) 2018年3月1日 下午2:42:00 zh_CN (Chinese/China) 2018년 3월 1일 오후 2:42:00 ko_KR (Korean/Korea) 1 марта 2018 г., 14:42:00 ru_RU (Russian/Russia) 1 de março de 2018 14:42:00 pt_BR (Portuguese/Brazil) 1 de marzo de 2018, 2:42:00 p.m. es_CO (Spanish/Colombia)
Also note that the final output can also be affected by the user's device settings. For example, in iOS, the user can specify whether they want time to be shown in 12 or 24-hour format. This setting overrides the typical display for their locale. In macOS a user can change the date and time formatting in the Date & Time preferences. This is actually another reason to use date and time styles with a date formatter as opposed to a specific date format.
Earlier I mentioned the use of the description(with:)
method of Date
to print a date
in a format using the current timezone and locale. The output of this is the same as using a
DateFormatter
with both the date and time styles set to .full
.
While using date and time styles is always the preferred method for showing dates to a user, there are cases where
you actually do need to convert a Date
to a String
in a very specific format. You
might need to send a date to a server or some API and the server or API expects a date string in a specific format.
In such cases you need to set the dateFormat
property of the DateFormatter
instead of
setting the styles.
In order to use a date format, you need to provide the exact date formatting pattern required to get the desired
results. If you look at the reference documentation for the dateFormat
property, there is a link
to the
Data Formatting Guide. This in turn has a link to the
Date Formatters page. Under the "Fixed Formats" section is
a list of OS X and iOS versions with a link to Unicode Technical Standard #35 describing all of the supported
date format patterns. The latest version can be found here. Note that the latest version may have features not yet supported by your code.
Once you decide what format you wish to have the date converted to, you need to build the date format using the appropriate field symbols, punctuation, and any literal text.
Here is an example of converting a Date
into a String
using a fixed format:
// Swift
let dff = DateFormatter()
dff.dateFormat = "yyyy-MM-dd HH:mm:ss"
dff.locale = Locale(identifier: "en_US_POSIX")
print(dff.string(from: date))
// Objective-C
NSDateFormatter *dff = [[NSDateFormatter alloc] init];
dff.dateFormat = @"yyyy-MM-dd HH:mm:ss";
NSLog(@"%@", [dff stringFromDate:date]);
Here is the result for a date representing March 1, 2018 at 2:42 pm local time.
2018-03-01 14:42:00
Note that the date formatter defaults to showing the date in the user's current timezone. If you want the result
to appear in a specific timezone, you need to set the timeZone
property of the date formatter.
Also note the use of the special locale of en_US_POSIX
. See the last bullet in the list below for
more on its use.
The following are some sample date formats and the result using a date representing March 1, 2018 at 2:42 pm in Eastern Daylight Time (UTC -5 hours).
yyyy-MM-dd 2018-03-01 h:mm a 2:42 pm yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ 2018-03-01T14:42:00.000-05:00 dd/MM/yy 01/03/18 d.M.y 1.3.2018 d-MMM-yyyy 1-Mar-2018 HH:mm:ss Z 14:42:00 -0500
Here are some important things to keep in mind when building your format string:
''
).M
is month. m
is minute. Use the wrong case and get the wrong result.y
. You probably never mean to use Y
. The two almost always give the same result leaving you to think Y
is correct. But it's not. Use y
unless you have a clearly understood need to use Y
.h
for a 12-hour format and use H
when you want a 24-hour format. When you do use h
for a 12-hour format, you probably also want to add a
to get AM/PM. And avoid using a
with H
.ss.SSS
. The number of S
after the decimal should match the number of decimals you want. But you may not get much after 3 decimal places.z
and v
. The resulting timezone abbreviations are not unique. It is much better to use a symbol that generates a numeric timezone offset or a timezone ID.en_US_POSIX
. This will ensure your fixed format is actually fully honored and no user settings override your format. This also ensures month and weekday names appear in English. Without using this special locale, you may get 24-hour format even if you specify 12-hour (or visa-versa). And dates sent to a server almost always need to be in English.
There are times when you wish to show a properly localized date to a user but none of the standard styles
meet your needs. For example, you wish to show just the month and day but not the year. DateFormatter
supports this through a format template. Using the same symbols used for the dateFormat
property,
you provide a template that contains just the desired formatting symbols. You don't provide any other text or
punctuation and you don't worry about the order of the fields. DateFormatter
will convert the
provided template into the most appropriate date format based on the formatter's locale. Be sure you set the
locale, if needed, before setting the template.
The following code will format a date showing the month, day, and hour.
// Swift
let dft = DateFormatter()
dft.setLocalizedDateFormatFromTemplate("MMMdh")
print(dft.string(from: date))
// Objective-C
NSDateFormatter *dft = [[NSDateFormatter alloc] init];
[dft setLocalizedDateFormatFromTemplate:@"MMdh"];
NSLog(@"%@", [dft stringFromDate:date]);
Here are some example results, followed by the locale and resulting date format generated from the template, for a date representing March 1, 2018 at 2:42 pm:
Mar 1, 2 PM en_US (English/USA) MMM d, h a 1 Mar, 2 pm en_GB (English/Great Britian) d MMM, h a 1. März, 2 PM de_DE (German/Germany) d. MMM, h a 1 mars à 2 PM fr_FR (French/France) d MMM 'à' h a ١ مارس، ٢ م ar_EG (Arabic/Egypt) d MMM، h a 3月1日 下午2时 zh_CN (Chinese/China) MMM d, h a 3월 1일 오후 2시 ko_KR (Korean/Korea) MMM d일 a h시 1 марта, 2 ПП ru_RU (Russian/Russia) d MMM, h a 1 de mar 2 PM pt_BR (Portuguese/Brazil) d 'de' MMM h a 1 de mar., 2 p.m. es_CO (Spanish/Colombia) d 'de' MMM, h a
There is an ISO standard for date formats known as ISO 8601. When working with such dates you should consider using the ISO8601DateFormatter
class instead of the more general DateFormatter
class.
If you have two dates representing a date interval, you can use a DateIntervalFormatter
to create
a properly localized representation of the date interval.
There are actually ways to convert a Date
to a String
without using a
DateFormatter
. I'm pointing these out because you must never use these to display a date to the user
or to create a date string to be passed off to some server or other API. The following options must only ever
be used for debugging purposes only. In the following lines of code, assume date
is some instance of
a Date
.
// Swift
let str = "\(date)"
let str = String(describing: date)
let str = date.description
let str = date.description(with: nil)
let str = date.description(with: .current)
let str = date.debugDescription
// Objective-C
NSString *str = [NSString stringWithFormat:@"%@", date];
NSString *str = date.description;
NSString *str = [date descriptionWithLocale:nil];
NSString *str = [date descriptionWithLocale:NSLocal.currentLocale];
The reason these options should only be used for debugging is that none of the results are documented. And even
the documentation for Date description
states:
The representation is useful for debugging only.
There are times when your code obtains a string representation of a date and you need to convert that string into
an actual Date
instance. These date strings may come from a server or other API. Dealing with date
strings is common when receiving JSON data.
Creating a Date
from a String
is nearly identical to formatting a date into a string,
just in reverse. You need a date format that exactly matches the format of the date in string. This includes
any literal text and punctuation. And since you are parsing a fixed format, you should be using the special locale
of en_US_POSIX
with the date formatter.
See the section on Date Formats above for details on using a date format. The important thing to remember is that your format must exactly match the string you are trying to parse.
Here is a common example for a typical date string obtained in JSON data.
// Swift
let dateString = "2018-03-15T16:37:29Z" // the date string to be parsed
let df = DateFormatter()
df.locale = Locale(identifier: "en_US_POSIX")
df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssX"
if let date = df.date(from: dateString) {
// do something with the date
} else {
print("Unable to parse date string")
}
// Objective-C
NSString *dateString = "2018-03-15T16:37:29Z"; // the date string to be parsed
NSDateFormatter *df = [[NSDateFormatter alloc] init];
df.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
df.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssX";
NSDate *date = [df dateFromString:dateString];
if (date) {
// do something with the date
} else {
NSLog(@"Unable to parse date string");
}
There are a few things to notice here:
ISO8601DateFormatter
.Z
at the end of the date string is not some random literal character. That is actually the timezone of the date. Z
means "Zulu" (the phonetic name for the letter "Z"). This is Zulu time (which comes from zero time) which is a synonym for UTC time.'T'
to deal with the literal T
in the date string.X
to process the timezone. You will likely see many code samples that use a Z
in this case. Technically you should use the X
symbol to parse a timezone value of Z
but a date formatter is more forgiving when parsing a date string. A given symbol can typically parse more formats than it specifically represents. But it's best to use the proper symbols.date(from:)
return an optional Date
. Avoid force-unwrapping this call since it is possible the date string may not match the expected format. When working with data from a server or other API, code defensively. Never assume the data you receive is in a specific format. Things change and you don't want your code crashing because the server changed its data. Handle the unexpected result more gracefully.
Please note that all date string parsing should be done using a date format and the special locale of
en_US_POSIX
. It would be very unusual to parse a date string using date and time styles. This is
because those formats are not fixed in addition to depending on the locale used to create the date string.
Along these lines, never use a date string as "data". If you obtain a fixed format date string from a server or
other API, convert the string to a Date
and use that date as your data. Do all data processing on
that date. If, later, you need to display that date data to the user, format the date into a string using a
DateFormatter
and appropriate date and time styles.
A task that appears occasionaly is the need to convert a date string from one format to another. This is easily
accomplished in two steps. 1) Convert the original date string to a Date
. 2) Format the date into a
string.
Here is sample code that converts the date string 2018-03-15 09:45:30 +02:00
into a more user friendly
format for display in the app:
// Swift
let dateString = "2018-03-15 09:45:30 +02:00" // the date string to be parsed
print(dateString)
let df1 = DateFormatter()
df1.locale = Locale(identifier: "en_US_POSIX")
df1.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZZZ"
if let date = df1.date(from: dateString) {
print(date)
let df2 = DateFormatter()
df2.dateStyle = .short
df2.timeStyle = .short
let string = df2.string(from: date)
print(string)
} else {
print("Unable to parse date string")
}
// Objective-C
NSString *dateString = "2018-03-15 09:45:30 +02:00"; // the date string to be parsed
NSLog(@"%@", dateString);
NSDateFormatter *df1 = [[NSDateFormatter alloc] init];
df1.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
df1.dateFormat = @"yyyy-MM-dd HH:mm:ss ZZZZZ";
NSDate *date = [df1 dateFromString:dateString];
if (date) {
NSLog(@"%@", date);
NSDateFormatter *df2 = [[NSDateFormatter alloc] init];
df2.dateStyle = NSDateFormatterShortStyle;
df2.timeStyle = NSDateFormatterShortStyle;
NSString *string = [df2 stringFromDate:date];
NSLog(@"%@", string);
} else {
NSLog(@"Unable to parse date string");
}
The result of course depends on the user's locale and timezone. For a user in New York City with a locale of
en_US
and a timezone of UTC-4 (Daylight Saving time is in affect on this date), the output is:
2018-03-15 09:45:30 +02:00
2018-03-15 07:45:30 +0000
3/15/18, 3:45 AM
Note how none of the times seem to match the original date string. But all of the times are correct. The original date string is from a timezone that is two hours ahead of UTC. The second print is showing the description of the date which is being shown in the UTC timezone, hence the two hour difference from the original string. The third print is showing the final string which is from the user's timezone, four hours behind UTC, for a total of six hours from the original string.