Handling Time Zones in iOS Apps

Time zones are one of those tricky programming problems. It can be difficult to understand what’s going on and maddening to debug, especially if you have customers all over the world.

The key is understanding that the Date() struct in Foundation returns an absolute timestamp in UTC. UTC is a time standard (i.e. no countries adopt it as a time zone—they would use GMT which is a time zone but, for our purposes, is identical to UTC).

What this means for app development is that the time and date you get back from the Date() struct might not match the local time and date where your users are.

At the time of writing it is 4pm here in Austin, Texas on the 3rd January. In Brisbane, Australia it’s 8AM on the 4th December. In London, 10pm on the 3rd.

The Date() struct will return 10pm on the 3rd (London is on GMT as it’s winter—in summertime it switches to British Summer Time, or BST).

Many of the Foundation classes are designed to work with time zones and if you are just using them in a transient way (e.g. getting the current time and putting it immediately in a DateFormatter instance), then you probably don’t have to worry about it.

The problems arise when you start storing dates.

If a hypothetical Walleteer turns up at Brisbane airport on their way to London and buys a coffee at 8AM on the 4th, then their memory is always going to be that the entry was made on the morning of the 4th.

If I, the Foolish Trail Wallet Developer, just store the date and no other information, then when they arrive in London and open the app again, their coffee expense will suddenly have moved to the 3rd at 10pm.

They get mad. I get email. No one wins.

As a Smart Trail Wallet Developer, I therefore designed my data store to keep track of three pieces of information:

  1. The UTC timestamp when it was entered (in Brisbane, it would look something like 2018-12-15 22:00:00 even if it’s actually the morning of the 16th for them).
  2. A textual representation of the time zone, returned from the TimeZone.current.identifier property (e.g. Australia/Brisbane)
  3. Finally, and crucially, the UTC timestamp of their current date and time as if it was in the GMT time zone. This would convert the above date to 2018-12-16 08:00:00.

All of the date queries in Trail Wallet are done based on this last field.

This is, of course, not an accurate representation of the absolute time they entered the amount (which is why I keep the original date and the time zone) but it is what makes most sense to human beings using the app day to day.

The alternative would be to convert the original date using the saved time zone information on the fly, but I find switching everything to a single time zone (UTC in this case) and saving that to the database to be much simpler and faster as I can do queries directly on this field when requesting data.

Converting the Local Date and Time to UTC


let now = Date()
// 1.
let components = Calendar.current.dateComponents(in: TimeZone.current, from: now)
// 2. 
components.timeZone = TimeZone(abbreviation: "UTC")!
// 3.
let localDateAsUTC = components.date!
  1. This Calendar method returns the date components from the passed date as they appear in the current time zone. The returned components will have the time zone set to the current time zone.
  2. If I then change the time zone of the components to UTC…
  3. …I can extract the new date as if the components were in the GMT time zone.

The implementation I use in Trail Wallet is a bit more complicated than this. So far, it has not caused any major issues but thinking this way has saved my sanity when dealing with time zones in apps.

Technically inaccurate but very pragmatic.

There Are Always Downsides

The caveat is that I have to remember that anything that displays a date must also be set to the UTC time zone.

For example, a DateFormatter instance will use the current time zone by default when displaying the date. Its timeZone property needs to be set to UTC to show the “correct” time (i.e. the time that the user is expecting to see).

let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .full
formatter.string(from: localDateAsUTC) // !!! This could display an incorrect date/time !!!
formatter.timeZone = TimeZone(abbreviation: "UTC")!
formatter.string(from: localDateAsUTC) // This will always be correct (relative to the user)

Conclusion

Time zones are hard. Test early and often.

Good luck.