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