At Touchwonders we strive to make apps that withstand any conceivable user scenario. What can happen with the app if the user is travelling, moving in and out of network reachability? How should the app behave if the user changes authorization for a required service such as location tracking? Is the design affected by any accessibility setting?
We do our best to foresee all of these scenarios before we start writing code, but every once in a while it happens that a new “edge case” surprises us. As was the case with the new Weeronline app, when one of our testers set his iPhone to 12-hour clock notation, and suddenly half of the weather widgets stopped working.
The setting, which can be found in the iOS Settings app under General → Date & Time → 24-Hour Time (switched off), had two effects on the Weeronline app. Immediately obvious was that certain widgets (not all) could not load their weather data properly, displaying a failure message (which was designed too, so that at least worked properly). Probably the parsing of Weeronline's service API response failed. Less severe but still very undesirable were various labels which were designed to snugly fit a formatted time such as “22:00” but which overflowed when trying to fit its 12-hour counterpart “10:00 pm”.
Date & Time Formatting
To understand the problem, let us observe step by step how date and time formatting on iOS works, and which classes are involved. The most important players in this story are
NSDateFormatter. In their documentation, Apple describes
NSDate as follows:
It encapsulates a single point in time, independent of any particular calendrical system or time zone.
It represents an invariant time interval relative to an absolute reference date (00:00:00 UTC on 1 January 2001).
NSDateFormatter is used for the following tasks:
Create string representations of
Convert textual representations of dates and times into
A string representation of an
NSDate is made for each date or time that you see in an app. In Weeronline you can find this in the rain radar, the rain graph and the 48 hour detail forecast where a time is displayed, as well as in the 14 day detail forecast which displays the dates for the upcoming 14 days. The dates were all unaffected by the 12-hour setting, but most of the time labels in the mentioned widgets overflowed due to an added am/pm.
The other task, converting a textual representation of a date and time into an NSDate, is done whenever we make a request to Weeronline’s servers for weather forecast data and process the response. Most of these responses contain weather forecast data which is accompanied by a date/time string identifying for which time the forecast or measurement is. We need to convert this to an NSDate to be able to properly display the data in a graph or animate it on a map, and to format it again to a string that would only contain hour and minute information.
After establishing that the 12-hour setting had an undesirable influence on
NSDateFormatter, further investigation revealed that there are three classes that play an important role in the behaviour of
These three classes together represent the context in which date and time formatting is performed. To have a good understanding of these classes, let us look at how device settings, user settings and developer settings are combined.
Your iPhone is set to a specific language. This setting can be found in the Settings following General → Language & Region → iPhone Language. My iPhone is set to English, so all apps and settings but also months and weekdays are displayed in English.
A bit further down in the Language & Region settings screen you'll find Region, which on my iPhone is set to “Netherlands”, because I'm Dutch. What this affects is displayed nicely in the bottom of the Language & Region screen: the Dutch use a 24-hour time format, a date is formatted as “Weekday Monthday Month Year”, prices are in Euros and thousand and decimal separators are . and , respectively. These preferences are what is described by an
Our problem most likely has something to do with
NSLocale, but just for sake of completeness, let's also briefly look at calendars and time zones. Below Region you’ll find the Calendar, which is most likely set to Gregorian, but just as well can be something different (f.i. Buddhist, Japanese or Hebrew).
NSCalendar allows for date calculations such as finding the weekday exactly one year from now, and handles calendar differences such as how years are counted or on which date a year starts.
Your time zone is set under General → Date & Time, and most likely is updated automatically. Apple luckily provides us developers with a nice class,
NSTimeZone, to deal with the complexity of time zones.
For each of these three classes, an instance can be accessed to be used in various formatting tasks. The accessors (in Swift) are:
NSLocale.currentLocale() NSCalendar.currentCalendar() NSTimeZone.systemTimeZone()
These will give you an instance representing the device settings overlaid with the user settings. There are a few other accessors, such as:
NSLocale.systemLocale(), which returns an NSLocale that is based on the device settings but unaffected by any user overrides.
NSTimeZone.defaultTimeZone(); User and developer can specify an app-specific time zone, accessible through this property.
Configuring Your DateFormatter
Now these three players have been introduced, let’s get back to the
NSDateFormatter. An instance of this class has three properties: locale, calendar and timeZone. These three are set by default to the currentLocale, currentCalendar and defaultTimeZone, respectively. This is very convenient if you want to convert an
NSDate to precisely the format the user expects from his/her specific configuration; just call
NSDateFormatter() to instantiate one and you’re all set (or even simpler, call the class method
Problems occur if you need to parse date strings that you receive from some service, such as the Weeronline server. You instantiate an
NSDateFormatter, but if you do not specify anything, formatting will only go well if the service serves dates which are in formatted with exactly the same time zone, calendar and locale as the device on which your app runs. So let’s start with the obvious step and configure a specific time zone. Weeronline provides all dates and times in Coordinated Universal Time (UTC), so we create an instance of
NSTimeZone for UTC and set that on the dateFormatter:
let dateFormatter = NSDateFormatter() dateFormatter.timeZone = NSTimeZone(abbreviation: “UTC")
Works like a charm… Unless you are a Buddhist, then the weather forecasts are suddenly years off! So let’s specify that we always want to use the Gregorian calendar:
dateFormatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)
This should cover it, right? No. This is where it went wrong with the 12-hour setting. To illustrate how an
NSLocale influences our dateFormatter, we use the following
NSDateFormatter class method call:
NSDateFormatter.dateFormatFromTemplate("HH:mm", options: 0, locale: dateFormatter.locale)
The specified format states it should format an
NSDate to two-digit 24-hour (“HH”), followed by two-digit minutes ("mm"). When you have your iPhone configured to 24-hour time, this returns “HH:mm”, but when configured to 12-hour time, this suddenly returns “h:mm a”, stating a single 12-hour value ("h"), followed by two-digit minutes and then the appropriate AM/PM symbol ("a") as specified by the
When converting a
NSDate into a string, this ensures that your specified dateFormat is adjusted according to the user’s preferences for date and time formatting. We saw this happening in the various labels that were too narrow to appropriately fit the AM/PM after the time; the entire design was based on 24-hour time strings. Much worse however is that this automatic adjusting of your format string also applies when you try to convert a string into an
NSDate. If your string doesn’t match your format (for example “13:44”),
dateFromString() will return
nil instead of an
NSDate. Hence our data models had no dates to work with and signalled failure.
Setting A Locale
We know exactly what date format is returned by the server, so to avoid any user preference influencing our date parsing, we have to set a specific locale on the dateFormatter. But what will that be? There are plenty of locale identifiers to choose from. But almost all locales refer to a combination of language and region which, similar to time zones, are liable to change. It won’t happen very often, but it is well possible that a country, region or language decides to change the way in which dates and times are formatted, just as a country can decide to practice daylight saving time or not. For this reason a specific locale was designed to yield US English results but which is invariant in time, meaning it will never change, regardless of any regional or language changes in the future. It is unaffected by user or system preferences, and is also invariant between machines, meaning it works exactly the same on any platform. The identifier for this locale is “en_US_POSIX” (see also Apple's Technical Q&A 1480).
The above described approach to date parsing worked well for the Weeronline project, allowing us to parse different formats consistently. This was necessary because the various Weeronline service API’s did not all use the same format. Some of these API’s expressed date and time according to the international ISO 8601 standard. If all dates would have been in this format, we could have just used the ISO8601DateFormatter and never had to worry about locales, calendars and time zones. Similarly, a commonly used approach is to just use a Unix timestamp, which is a number describing the seconds that have elapsed since 00:00:00 January 1st 1970, UTC. For this the
NSDate class has a convenient initializer:
Whichever way you choose to approach your date and time formatting, I think it is important to be aware of all the influences that a user’s language, region, location, and personal preferences can have on this process. With that in mind, happy date-coding!