Time zones are convenient, but they are usually a pain for developers. Numerous bugs have made it into production because of time zones. The good news is that Apple's Foundation framework makes working with time zones pretty simple. In this episode, we take a look at Foundation's TimeZone struct and how to use it in a project. Fire up Xcode and create a playground if you want to follow along with me.

Creating Time Zones in Swift

The TimeZone struct is easy to use. As its name suggests, a TimeZone object encapsulates the details of a time zone. To create a TimeZone object, you pass the time zone's identifier or the time zone's abbreviation to the initializer. This is the first hurdle you need to cross. How do you know what the identifier or abbreviation of a time zone is?

The TimeZone struct exposes a static property, abbreviationDictionary, that returns a dictionary of key-value pairs. Each key-value pair represents a time zone with the key being the time zone's abbreviation and the value being one of the identifiers of the time zone.

Add an import statement to the playground and print the value of the static abbreviationDictionary property to Xcode's console.

import Foundation

print(TimeZone.abbreviationDictionary)

The output should look something like this.

[
  "NZDT": "Pacific/Auckland",
  "BRT": "America/Sao_Paulo",
  "HKT": "Asia/Hong_Kong",
  "CAT": "Africa/Harare",
  "KST": "Asia/Seoul",
  ...
]

Another option is to inspect the value of the static knownTimeZoneIdentifiers property. As the name suggests, this static property returns an array that contains the time zone identifiers the TimeZone struct supports.

import Foundation

print(TimeZone.knownTimeZoneIdentifiers)

The list is much longer than the one returned by the static abbreviationDictionary property. A time zone has one abbreviation while it can have multiple identifiers.

[
  "Africa/Abidjan",
  "Africa/Accra",
  "Africa/Addis_Ababa",
  "Africa/Algiers",
  "Africa/Asmara",
  ...
]

I'm located in Europe so let's create a time zone passing CET to the init(abbreviation:) initializer. The initializer is failable, which makes sense since the initialization should fail if you pass an invalid abbreviation to the initializer.

TimeZone(abbreviation: "CET")

We can also create a TimeZone object for the CET time zone by passing Europe/Paris to the init(identifier:) initializer. This initializer is also failable for the same reason.

TimeZone(identifier: "Europe/Paris")

There is one other option to create a time zone, that is, by passing a temporal offset to the init(secondsFromGMT:) initializer. We can create a TimeZone object that represents the CET time zone by passing 3600 to the initializer.

TimeZone(secondsFromGMT: 3600)

We can ask the TimeZone object for its localized name by invoking its localizedName(for:locale:) method. The first argument is of type NSTimeZone.NameStyle and defines the format of the localized name. The second argument is the locale to use to localize the time zone's name. If you pass nil as the second argument, then the result only includes the temporal offset from Greenwich Mean Time (GMT).

if let timeZone = TimeZone(abbreviation: "CET") {
    timeZone.localizedName(for: .standard, locale: nil)         // GMT+01:00
    timeZone.localizedName(for: .standard, locale: .current)    // Central European Standard Time
}

Working with Time Zones in Swift

You rarely use the TimeZone struct in isolation. You typically use it in conjunction with another API, such as the DateFormatter class. We create a DateFormatter instance, set its timeZone property, and define a date format. We use the date formatter to convert a Date object to a human-readable string, respecting the user's time zone.

import Foundation

if let timeZone = TimeZone(abbreviation: "CET") {
    timeZone.localizedName(for: .standard, locale: nil)
    timeZone.localizedName(for: .standard, locale: .current)

    let dateFormatter = DateFormatter()

    dateFormatter.timeZone = timeZone
    dateFormatter.dateFormat = "MMM d, h:mm a"

    dateFormatter.string(from: Date()) // Sep 26, 11:22 AM
}

Let's replace the CET time zone with the PST time zone. The result confirms that the date formatter takes the value of its timeZone property into account.

import Foundation

if let timeZone = TimeZone(abbreviation: "PST") {
    timeZone.localizedName(for: .standard, locale: nil)
    timeZone.localizedName(for: .standard, locale: .current)

    let dateFormatter = DateFormatter()

    dateFormatter.timeZone = timeZone
    dateFormatter.dateFormat = "MMM d, h:mm a"

    dateFormatter.string(from: Date()) // Sep 26, 2:22 AM
}

Time Zones and Unit Testing

The TimeZone struct can at times be indispensable for writing reliable unit tests. In the following example, we create a date using the Calendar and DateComponents APIs. Note that we set the timeZone properties of the Calendar and DateComponents instance to ensure that the resulting date is always the same, regardless of the system the unit test is run on.

import Foundation

var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(abbreviation: "CET")!

var dateComponents = DateComponents()
dateComponents.day = 1
dateComponents.month = 9
dateComponents.year = 2022
dateComponents.hour = 2
dateComponents.minute = 30
dateComponents.timeZone = TimeZone(abbreviation: "KST")

calendar.date(from: dateComponents) // Aug 31, 2022 at 7:30 PM