Updated: December 2021

Overview

Since its introduction to the Apple development platform in 2014 (announced at WWDC 2014), Swift continues to mature rapidly due to its open development community (swift.org). As such, Swift code is clean code with clear consistent syntax and strongly typed, compiles to a fast executable, and includes many advanced design and coding principles. It is truly a great modern programming language!

Alas, developing code for real-world apps is hard and still a humbling experience. This post focuses on the insanely complex rabbit hole known as date and time and its implementation in Swift. Some of the complexity of getting date and time correct is humorously presented by Tom Scott in the following video.

An enlightening article by Dave Delong (Your Calendrical Fallacy Is …) lists some of the false assumptions regarding date and time including:

  • Are days always 24 hours long?
  • Does an hour ever occur twice in a single day?
  • Does every day have a midnight?
  • Are days in a work week always contiguous?
  • Does a year always consist of 12 months?
  • Does a leap year always occur every 4 years except when the year is evenly divisible by 100, unless it is also divisible by 400?
  • Does each year always have just one New Year’s day?
  • Are timezones always on the hour mark?
  • Do the rules for Daylight Saving Time change?

As referenced in the video, updates to the Timezone Database still occur. The recent updates (Jan 31, 2018) to the Timezone Database are highlighted in an article by Dave Delong (The 2018c Timezone Datebase Update). Ideally, time zones would be equally spaced around the earth every 15 degrees in longitude. Not even close! The following world map of current timezones highlights this complexity.

And, finally, the overall weirdness of calendars, dates and times is examined philosophically by Dave Delong (Intercalation).

Bottom-line: Use the blackbox approach. Always use Calendar, Date, DateFormatter, Locale, and DateComponents), which are all built on top of the ICU (International Components for Unicode) libraries.

The software examples in the next several sections are included as an Xcode playground located on GitHub.

Basic Date and Time

In Swift, a Date() value encapsulates a single point in time, independent of any particular calendrical system or time zone. To accomplish this, a Date() value is stored as a Double (64-bit floating point number) counting the number of seconds as a relative offset from the reference date of January 1, 2001 at 00:00:00 UTC (Coordinated Universal Time).

So let’s begin with getting the current date/time and printing out the default result as an ISO8061-formatted string referenced to UTC.

let now = Date() // current date and time
print(now) // e.g., 2021-10-05 18:17:50 +0000

To see the actual number of seconds relative to Apple’s reference date (January 1, 2001 at 00:00:00 UTC), the code needs to be more specific. The fractional part of the result represents the fractional part of a second.

let now = Date() // current date/time (e.g., 2021-10-05 18:17:50 +0000)
let dateRef = now.timeIntervalSinceReferenceDate
print(dateRef) // number of seconds since reference date (e.g., 655150670.057823)

To see the date given an integer (e.g., truncated to one second resolution) representing the number of seconds relative to Apple’s reference date (January 1, 2001 at 00:00:00 UTC), the code to convert to a date is still straightforward.

let seconds: Int = 661573661 // integer example with resolution of 1 sec
let timeSeconds = TimeInterval(seconds)
let date = Date(timeIntervalSinceReferenceDate: timeSeconds)
print("date: \(date)") // 2021-12-19 02:27:41 +0000

Note to Unix users: To see the actual number of seconds relative to the Unix’s reference data known as ‘epoch date’ (January 1, 1970 at 00:00:00 UTC), the code needs to be specific with use of the ‘timeIntervalSince1970’ modifier.

let now = Date() // current date/time (e.g., 2021-10-05 18:17:50 +0000)
let dateEpoch = now.timeIntervalSince1970
print(dateEpoch) // number of seconds since epoch data (e.g., 1633457870.057823)

Standard Date and Time Formatting

However, the real feature-rich functionality benefit in Swift comes from using DateFormatter, Locale, and Calendar. This is how Swift supports timezones, localized date formats, and calendar arithmetic (e.g., 1 month ago, 5 days later). To use DateFormatter, we need to create a DateFormatter object. Then, we can specify various date and time formatting options as shown in the examples below (based on en_US in Seattle).

The above options provide commonly used combinations of date and time formats. We can access just date only (timeStyle set to .none) or time only (dateStyle set to .none) as the following examples show (based on en_US in Seattle).

myDateFormat.dateStyle = .medium
myDateFormat.timeStyle = .medium
print(myDateFormat.string(from: now)) // date/time, both medium format (e.g., Oct 5, 2021 at 11:45:20 AM)
myDateFormat.dateStyle = .long
myDateFormat.timeStyle = .none
print(myDateFormat.string(from: now)) // date with long format (e.g., October 5, 2021)
myDateFormat.dateStyle = .none
myDateFormat.timeStyle = .short
print(myDateFormat.string(from: now)) // time with short format (e.g., 11:45 AM)

Localization of Date and Time

Now for the cool part. We can create a Locale object that will auto-magically localize the date and time formatted results.

let myDateFormat = DateFormatter()
let now = Date()
myDateFormat.dateStyle = .medium
myDateFormat.timeStyle = .none
myDateFormat.locale = Locale(identifier: "en_US")
print(myDateFormat.string(from: Date())) // Oct 5, 2021
myDateFormat.locale = Locale(identifier: "fr_FR")
print(myDateFormat.string(from: Date())) // 5 oct. 2021
myDateFormat.locale = Locale(identifier: "ja_JP")
print(myDateFormat.string(from: Date())) // 2021/10/05

In iOS, localization changes will cause an app restart. Typically, the DateFormatter will re-instantiate with an updated locale. However, we can do better and ensure auto-updating by adding the following code:

let myDateFormat = DateFormatter()
let now = Date()
myDateFormat.dateStyle = .medium
myDateFormat.timeStyle = .none
myDateFormat.locale = Locale.autoupdatingCurrent

Custom Date and Time Formatting

In addition to the standard date and time formats associated with DateFormatter, custom date and time formats are entirely possible with much flexibility.

let myDateFormat = DateFormatter()
let now = Date()
myDateFormat.dateFormat = "yyyy-MM-dd HH:mm:ss"
print(myDateFormat.string(from: now)) // 2021-10-05 13:05:35
myDateFormat.dateFormat = "MM/dd/yy h:mm a"
print(myDateFormat.string(from: now)) // 10/05/21 1:05 PM

An extensive suite of iOS Swift custom formatting options is available at NSDataFormatter.

SwiftUI Date and Time

Beginning with iOS15, we get some simplified date/time formats without the need for a date formatter object. And we still have the existing date formatter and all its options.

let now = Date()
print(now.formatted()) // 10/5/2021, 3:46 PM
print(now.formatted(date: .complete, time: .omitted)) // Tuesday, October 5, 2021
print(now.formatted(.iso8601.dateSeparator(.dash))) // 2021-10-05T22:46:58Z
print(now.formatted(.dateTime.year().month())) // Oct 2021
print(now.formatted(.dateTime.year(.twoDigits).month(.wide))) // October 21
print(now.formatted(.dateTime.locale(Locale(identifier: "cs")))) // 5. 10. 2021 15:46

Dates and Codable

Matching date/time string formats when coding with ISO8601 data is different depending on whether the date/time string format is in whole seconds or includes fractional seconds (i.e., milliseconds).

let isoFormatter = ISO8601DateFormatter()
// Example 1: Using ISO8601 date formatter with date string that ends with whole seconds
let dateStringSeconds = "2021-10-06T11:27:55Z"
let newDateSec = isoFormatter.date(from: dateStringSeconds)
print(String(describing: newDateSec)) // 2021-10-06 11:27:55 +0000
// Example 2: Using ISO8601 date formatter with date string that ends with milliseconds
let dateStringMilliSeconds = "2021-10-06T11:27:55.341Z"
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let newDateMilli = isoFormatter.date(from: dateStringMilliSeconds)
print(String(describing: newDateMilli)) // 2021-10-06 11:27:55 +0000

Summary:

After spending some time learning about handling date and time in Swift and the Xcode development environment, the functionality that I needed for an app under development is relatively clean. I am especially pleased with the localization benefit with using Locale. It is important to always test both functionality and performance. When using DateFormatter, the recommendation for better performance is to create a single instance of DateFormatter. And, iOS 15 brings simplified date and time formatting to SwiftUI.