Dates, DateComponents and Formatting in Swift

Written by Reinder de Vries on March 16 2020 in App Development, iOS, Swift

Dates, DateComponents and Formatting in Swift

How do you work with date and time in Swift? In this tutorial, we’ll discuss how you can convert date/time to strings, and vice versa, how date/time formatting works, how to calculate time durations, and much more.

Here’s what we’ll get into:

  • How to work with Date, DateFormatter and DateComponents
  • The nitty-gritty of timezones, locales, and date/time formatting
  • How to add “+2 months” to a given Date object (and more…)
  • Calculating relative time strings, such as “2 months ago”
  • Constructing Date objects from date components

Ready? Let’s go.

  1. Get Current Date and Time With Swift
  2. Date to String: Formatting Dates & Locales
  3. String to Date: Parsing Dates & Timezones
  4. Creating Dates With DateComponents
  5. Calculating Date/Time Durations With DateComponents
  6. Further Reading

Get Current Date and Time With Swift

Let’s start with a simple example. How do you get the current date and time in Swift? Like this:

let now = Date()

Simple, right? The default time for a newly initialized Date object, like the one above, is the current date and time. When you create a new Date object with the Date() initializer, it has the current date and time.

Let’s discuss this Date type some more. Here’s what you need to know:

  • The Date type is part of the Foundation framework, so it’s a fundamental type to work with dates on iOS, macOS, tvOS, watchOS and Catalyst.
  • A Date object represents a single point in time, regardless of locale or calendar setting. As you’ll soon see, this fact is crucial to work with dates effectively.
  • A Date object has millisecond precision, i.e. it can represent points in time up to 1/1000th of a second. The default time unit for date/time APIs is the second (and fractions), which is OK for common use cases.

You can print out a date in the Console, like this:

let now = Date()
print(now)

Keep in mind that a Date object can be represented as a string, so it can be printed out, but that’s not the recommended approach to convert date to string (or vice versa). When you run the above code in Xcode, you’ll get the current date and time in ISO 8601 format (in your system’s timezone) as debug output.

If you’re familiar with working with date and time in app development, you may have heard of the term “Unix time” (or Unix timestamp). A Unix timestamp is (commonly) the number of seconds that have elapsed since January 1st, 1970 at 00:00:00 (midnight, UTC). Timestamps are commonly represented as 10-digit numbers, so it’s easy to store them in a database or simple Int or Double value.

But… there’s going to be a lot of “buts” in the rest of this tutorial. Because for every rule we establish, like “Unix timestamps fit in 10 digits”, there’s an exception you’ll have to take in account. Things like timezones, leap seconds, calendars, formatting and locales. It’ll be fun!

Learn how to build iOS apps

Get started with iOS 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

Important: Don’t have anything to take away from this tutorial!? At least know the following guidelines for working with date and time:

  • Don’t use calculations like 60 * 60 * 24 for time intervals, because that’ll go awry for leap seconds
  • Store dates as native Date objects or as timestamps, but know that timestamps aren’t perfect
  • Store dates in UTC/GMT whenever you can, only use your local timezone if you’re 100% certain that your app never crosses timezones
  • When representing dates as strings (i.e., for your app’s users), take in account an iPhone’s locale and calendar settings

Date to String: Formatting Dates & Locales

OK, let’s move on. You’ve got a Date and you want to display it to your app’s users. How do you convert a date to a string?

Here’s how:

  1. Create a DateFormatter object (the right way)
  2. Convert the date to string with the formatter

Let’s take a look at an example:

let now = Date()

let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .full

let datetime = formatter.string(from: now)
print(datetime)
// Wednesday, March 11, 2020 at 3:25:11 PM Central European Standard Time

What’s going on? First, we got the current date and time. Then, we created a DateFormatter() object and provided it with values for 2 properties: dateStyle and timeStyle. Then, we called the function string(from:) on the date formatter, provided it the Date object, and printed out the resulting string datetime. Awesome!

You can choose from various styles with the dateStyle and timeStyle properties. Both take values from the DateFormatter.Style enum. You can combine dateStyle and timeStyle to include date or time or both.

Here’s an example of what both styles can do:

Date Time
.none (nothing!) (nothing!)
.short 3/11/20 3:32 PM
.medium Mar 11, 2020 3:32:07 PM
.long March 11, 2020 3:32:07 PM GMT+1
.full Wednesday, March 11, 2020 3:32:07 PM Central European Standard Time

The exact date format depend on a system’s locale, which is what we’ll discuss next. What’s a locale? In short, it’s a set of parameters that defines a user’s language, region (or country), and any additional settings or variations.

The above table was generated with the en_US locale, which means it uses the US English style of formatting date and time: AM/PM, and month/day/year. Compare this to different countries and locales around the world, that use a 24-hour clock, and day-month-year, and you see why locales are so important!

Want to know what your system’s locale is? Try to run the following code in iPhone Simulator, or on an iPhone, and check out the result.

let formatter = DateFormatter()
print(formatter.locale)

It’s good to know that the DateFormatter object will use the iPhone’s system locale by default. If you’re creating a date formatter object, and you use dateStyle and timeStyle, and then string(from:), you’re almost guaranteed to create a textual representation of date/time in the user’s locale.

What if you want to use a custom date/time format? Here’s how you can do that:

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "nl_NL")
formatter.setLocalizedDateFormatFromTemplate("dd-MM-yyyy")

let datetime = formatter.string(from: now)
print(datetime)
// Output: 11-03-2020

The above Swift code prints a date style that’s common in The Netherlands, with dashes - and day-month-year. First, we’re setting the locale to nl_NL, and then we call setLocalizedDateFormatFromTemplate(_:) with a custom date/time format.

What’s interesting, is that this function will take into account the locale of the user, while allowing you to set a custom date/time format. Keep in mind that you’ll need to set the locale prior to calling setLocalizedDateFormatFromTemplate(_:).

Also, be mindful of making mistakes with custom formatting symbols – if you accidentally use YYYY instead of yyyy, you’re bound to get interesting results…

You can also use the dateFormat property of DateFormatter to set a custom date/time format, but this is only advisable for parsing date time formats (see next section). The dateFormat doesn’t take a user’s locale into account, so it’s not suitable for printing user-readable date/time formats.

Note: You can find more information about the date/time formatting symbols, like MM or yyyy, on this page. Currently, iOS uses the “Unicode Technical Standard #35, tr35-31”, which can be found here.

A good rule of thumb is to use as little “hardcoded” values as possible, when working with date and time. This includes fixed time intervals, like 86400, and fixed date formats, like d-m-Y. Instead, use constants and properties like dateStyle = .medium.

String to Date: Parsing Dates & Timezones

In the previous section, we’ve looked at how you can convert a date to string. How do you convert a string back to a Date object? That’s what we’ll discuss in this section.

Why would you want to convert strings to dates? A few scenarios:

  • You’ve received a well-formatted ISO 8601 string from a JSON-based webservice, and you need to convert it to a Date in Swift
  • You’ve stored a Unix timestamp in a database, that you need to parse to Date, and subsequently display to your app’s users
  • Data you’re working with has some obscure date formatting, that you want to parse, and write back in a more sensible format

The good news is that parsing date strings, and converting them to Date objects, is as simple as working with a DateFormatter object. The bad news is that parsing date strings has a ton of caveats and exceptions.

Let’s look at a quick example:

let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy HH:mm:ss Z"

let datetime = formatter.date(from: "13-03-2020 13:37:00 +0100")
print(datetime!)
// Output: 2020-03-13 12:37:00 +0000

In the above code, we’ve created a date/time format: day-month-year and hour-minute-second-timezone. We’re using double digit padding, i.e. 1 AM is written as 01:00. With the function date(from:) we’re using the formatter to convert a string to the datetime object, of type Date, which is printed out.

If you look closely, you can already see a discrepancy between the date string and the printed Date object. We’ve specified 13:37, but when datetime is printed out, we’re getting a 12:37 back. Why is that?

The Mac that I ran the above code on is set to the Central European Time (CET), which is one hour past Coordinated Universal Time (UTC). Based on the +0000, we can also see that the printed datetime object doesn’t have a timezone offset – so it’s in UTC.

Differently said, we input the time in the CET timezone, but it’s printed back in the UTC (-1 hour) timezone. The Date object and the date/time strings are referencing the same point in time, though. It’s the formatting that’s different! Confusing…

You can verify a Mac or iPhone’s timezone with this code:

let tz = TimeZone.current
print(tz)
// Output: Europe/Amsterdam (current)

We can assert that the date/time string 13-03-2020 13:37 was parsed with the CET timezone. After conversion, when printed, it was output in the UTC timezone. Hence the one hour difference. Good to know!

Can we print out the date/time in the correct timezone? Yes! First off all, it’s smart to provide timezone information whenever you’re working with date/time strings. That way you can avoid any confusion around the string’s timezone and your system’s timezone. Like this:

let datetime = formatter.date(from: "13-03-2020 13:37:00 +0100")

In the above date/time string, we’ve added a standardized offset of +1 hour. Keep in mind that this won’t change the timezone of the Date object, because Date objects are timezone-agnostic and/or use UTC. A fundamental principle of working with date and time is displaying them correctly, and always storing them as UTC.

Here’s how we can print the date/time in the right timezone:

let datetime = ··· // 13:37:00

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "nl_NL")
formatter.setLocalizedDateFormatFromTemplate("dd-MM-yyyy HH:mm:ss")

let dateString = formatter.string(from: datetime!)
print(dateString)
// Output: 13-03-2020 13:37:00

What if we want to show the same datetime object in a different timezone? Here’s how:

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "nl_NL")
formatter.setLocalizedDateFormatFromTemplate("dd-MM-yyyy HH:mm:ss")
formatter.timeZone = TimeZone(abbreviation: "EST") // Eastern Standard Time

let dateString = formatter.string(from: datetime!)
print(dateString)
// Output: 13-03-2020 08:37:00

In the above Swift code, we’re printing out that same datetime object (of type Date), using the nl_NL locale, using the US Eastern Standard Time (EST) timezone. What’s important here is that the Date object hasn’t changed, it’s still that 13:37:00 time, which is 8:37 AM in the EST timezone…

If you’re working with date and time, and your data is consistently a few hours off, check if you’ve not made any mistakes with timezones. And yes, not all timezone offsets are in 1-hour increments!

All this nonsense with timezones and locales will make you wonder: can’t we just adopt a default that always works? YES! That’s the ISO 8601 date/time format, represented with the ISO8601DateFormatter class in Swift (and use UTC/GMT).

Here’s how you can print the current date/time:

let now = Date()
let formatter = ISO8601DateFormatter()
let datetime = formatter.string(from: now)
print(datetime)
// Output: 2020-03-13T14:29:52Z

And here’s how you can convert a date/time string to a Date object:

let formatter = ISO8601DateFormatter()
let datetime = formatter.date(from: "2020-03-13T14:29:52Z")
print(datetime!)
// Output: 2020-03-13 14:29:52 +0000

Store those ISO 8601 in a database, parse to/from using the iPhone’s locale and timezone settings, and you’re good! Sensible user-representable date/time strings for the masses!

Creating Dates With DateComponents

So far we’ve looked at converting dates to strings, and strings to dates, and formatting, locales, timezones, and a bunch of gotchas.

But what if you want to work with the individual parts of a date or time, like days, day of the week, or minutes. Or what if you want to construct a Date object from individual day, month and year values? That’s where the DateComponents struct comes in.

The DateComponents type encapsulates date/time information, in terms of units (or “components”), such as minute, hour, day, month and year. It’s also calendar- and locale-aware, which means it’s the ideal structure for creating and manipulating dates.

Creating a date with DateComponents, from individual units, is simple:

let components = DateComponents(calendar: Calendar.current, timeZone: TimeZone(abbreviation: "GMT"), year: 2020, month: 1, day: 1)
print(components.date)

In the above Swift code, we’re creating a date (January 1st, 2020) from individual units. We’re using the initializer DateComponents(···), which is incredibly flexible, to construct a date. We’re also explicitly providing the calendar and timezone.

On the second line, we’re printing out the components.date value. This is a Date object that’s constructed from the date/time components we put in. Parameters for the DateComponents(···) initializer are ordered from biggest (era) to smallest (nanoseconds), followed by ordinal units, like weekday.

Because not every date component combination can be used (i.e., February 30th), we can use the isValidDate property to check if the date we put in actually exists. This is also affected by different calendar types, such as Gregorian and Chinese calendars.

It’s recommended to always provide a Calendar object, such as Calendar.current, and in most cases, recommended to provide explicit timezone information, such as TimeZone.current or TimeZone(abbreviation: ···). This way you avoid confusion around timezone offsets, and valid dates in various calendars.

You can also use the DateComponents structs to get individual date components you’re interested in. Here’s how:

let now = Date()
let components = Calendar.current.dateComponents([.month, .day], from: now)
print(components)
// Output: month: 3 day: 14

In the above code, we’re using the dateComponents(_:from:) function to extract date components from now. The result is a tuple, assigned to components, which we can deconstruct further:

print("The month is: \(components.month!)")
// Output: The month is: 3

Each of the items you can add to the dateComponents(_:from:) function has a corresponding enumeration value, in the Calendar.Components type, which are identical to the parameters for the DateComponents(···) initializer.

Calculating Date/Time Durations With DateComponents

The DateComponents isn’t only useful to create dates – it’s also helpful for specifying durations. The DateComponents struct can both define points in time, as well as durations of time.

Here, check this out:

let now = DateComponents(calendar: Calendar.current, year: 2020, month: 2, day: 1)
let duration = DateComponents(calendar: Calendar.current, month: 2)
let later = Calendar.current.date(byAdding: duration, to: now.date!)
print(later)
// Output: 2020-04-01 00:00:00

What’s going on here? In short, we’re adding a duration of 2 months to the date February 1st, 2020. The duration date components object is used to add a date period to now. As a result, we’re moving 2 months into the future: from February 1st to April 1st.

Remember when we discussed that you shouldn’t calculate date periods by a fixed number of seconds? Don’t calculate “2 months later” by adding 60 * 60 * 24 * 30 * 2 seconds to a February 1st.

Why not? Because 2020 is a leap year, and not all months are 30 days! February has 29 days, so adding any number of days to a date period that crosses February 29th, results in the wrong date calculation. The same is true for that 86400 seconds in a day rule – don’t use it!

Similarly, we can also use individual date components to add more units to a specific date. Like this:

let later = Calendar.current.date(byAdding: .month, value: 2, to: now.date!)

In the above code, we’re directly adding 2 months to now.date. This step-based approach is useful if you’re working with date/time units directly.

And last but not least… How do you calculate a relative “2 months ago”-like string from a given time period? This is how:

let date:Date! = DateComponents(calendar: Calendar.current, year: 2020, month: 1, day: 1).date
let units = Array<Calendar.Component>([.year, .month, .day, .hour, .minute, .second])
let components = Calendar.current.dateComponents(Set(units), from: date, to: Date())

for unit in units
{
    guard let value = components.value(for: unit) else {
        continue
    }

    if value > 0 {
        print("\(value) \(unit)s ago")
        break
    }
}

What’s going on?

  • First, we’re calculating a Date object based on the January 1st, 2020 date using the DateComponents struct. We want to know how many months, days, etc. have elapsed since that day.
  • Then, we’re constructing an array with the date units that we’re looking for. It’s type is [Calendar.Component], an array of Calendar.Component values.
  • Then, we’re calculating the time period between date and now, using the DateComponents (as duration). We’re quickly converting the units array to a Set. (We could iterate that set too, but it has no order, which won’t work for our algorithm.)
  • Finally, we’re iterating over the individual date components. The key is to check these components one by one, biggest to smallest, and to check if one is bigger than zero. When it is, we can say that date is roughly “X months” or “X days” etc. ago. If we find a match, we print out that unit and how many of them have passed. The for in loop is halted with break.

The principle we’re using in this algorithm, is figuring out how much time has passed between the given date and now. This duration is broken down into date/time components, like months, days, hours, etc.

We’re iterating these components, ordered biggest to smallest. This means we’ll encounter months before minutes, for example. As a result, when a duration is expressed in both months and days (non-zero), we’ll print out “X months ago”, because months is the bigger significant unit. When all units up to hours are zero, we’ll print out “X hours ago”, because hours is the biggest unit.

Differently said, when a non-zero unit is bigger than another unit, we’ll use that unit to roughly express the relative date/time duration. Which is exactly what “X days ago” needs to do. Awesome!

Learn how to build iOS apps

Get started with iOS 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

Further Reading

In this tutorial, we’ve looked at how to work with date/time in Swift.

We’ve discussed fundamental principles, like timezones and locales. You’ve learned how to work with staple date/time components, such as Date, DateFormatter and DateComponents. We’ve discussed what you can do with them, such as formatting date/time strings, parsing them, creating dates, and working with date/time durations.

Want to learn more? Check out these resources:

Reinder de Vries

Hi, I'm Reinder.
I help developers play with code.

Get the Weekly

Get iOS/Swift tutorials and insights in your inbox, every Monday.
  • This field is for validation purposes and should be left unchanged.

Most Popular

Browse Topics

Swift Sandbox

Code Swift right in your browser!
Go to the Swift Sandbox

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.

×

Build great iOS apps
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy