Introduction

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.

Table of Contents

Classes

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.

Date

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.

Locale

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"];

TimeZone

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.

DateFormatter

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.

Formatting Dates

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 covert 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.

Date/Time Styles

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 PMen_US (English/USA)
1 March 2018 at 14:42:00en_GB (English/Great Britain)
1. März 2018 um 14:42:00de_DE (German/Germany)
1 mars 2018 à 14:42:00fr_FR (French/France)
١ مارس، ٢٠١٨، ٢:٤٢:٠٠ مar_EG (Arabic/Egypt)
2018年3月1日 下午2:42:00zh_CN (Chinese/China)
2018년 3월 1일 오후 2:42:00ko_KR (Korean/Korea)
1 марта 2018 г., 14:42:00ru_RU (Russian/Russia)
1 de março de 2018 14:42:00pt_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.

Date Formats

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-dd2018-03-01
h:mm a2:42 pm
yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ2018-03-01T14:42:00.000-05:00
dd/MM/yy01/03/18
d.M.y1.3.2018
d-MMM-yyyy1-Mar-2018
HH:mm:ss Z14:42:00 -0500

Here are some important things to keep in mind when building your format string:

Format Template

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 PMen_US (English/USA)MMM d, h a
1 Mar, 2 pmen_GB (English/Great Britian)d MMM, h a
1. März, 2 PMde_DE (German/Germany)d. MMM, h a
1 mars à 2 PMfr_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 PMpt_BR (Portuguese/Brazil)d 'de' MMM h a
1 de mar., 2 p.m.es_CO (Spanish/Colombia)d 'de' MMM, h a

ISO 8601

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.

DateIntervalFormatter

If you have two dates representing a date interval, you can use a DateIntervalFormatter to create a properly localized representation of the date interval.

Other Options

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.

Parsing Date Strings

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:

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.

Converting Date Strings

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.