Keeping dates local
In our day-to-day life, dates are pretty straight forward - we read them all the time, make plans around them, and share them with other people. Apart from the occasional missed birthday party all of these date-based tasks go surprisingly smoothly. Which is remarkable when you stop to think how complex our date systems are. It works this smoothly because everyone is making some pretty large, unspoken assumptions around the dates that they see - what calendar is used, what timezone is used, the ordering of date elements, etc. While for most of us these assumptions rarely create issues in our day-to-day lives, if we want to build a system that uses dates we need to discover what assumptions we are making and remove them. Take for example the following date:
02/12/06
If you are from the UK then you would read this as:
2nd of December 2006
whereas if you are from the US then you would read this as:
February 12th, 2006
Both of these interpretations are valid however only one is correct. If the developer is from the UK then the former interpretation is correct and any US users are going to be either confused when they start seeing dates like 14/12/06
😕 or angry when they show up to an appointment on the wrong day 🤬. It's easy to fall into this date ordering trap especially if everyone in the development and testing teams is working off the same set of date assumptions/conventions - this is the real danger with assumptions, often we don't know that we are making them.
Getting to know what's local
When it comes to formatting dates we can choose to take the user's conventions into account or not, so for example, with the above date example 02/12/06
if we want to confuse our US users we could hardcode the date element order to match UK conventions by:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yy"
let formattedDate = dateFormatter.string(from: date)
It's not uncommon to see this approach to configuring a DateFormatter
and while there are valid scenarios for hardcoding the dateFormat
value (we will see an example of that later) I can't think of any valid scenarios for doing so when displaying a date to a user. By hardcoding the dateFormat
value we are instructing the DateFormatter
to ignore the date element ordering for the user's conventions and instead use the same element ordering for all users. By not meeting our user's date element ordering expectations we degrade that users experience by making something that should be as simple as reading a date into a surprise maths puzzle 📖. I've seen a number of novel but naive approaches on how to solve the expectation mismatch a hardcoded dateFormat
produces. These solutions tend to centre on two approaches:
- Add conditional logic and set a
dateFormat
value that's unique for each user's conventions. - Move the
dateFormat
value into the localisation.strings
file.
While it's possible to meet our user's expectations with either of these solutions both have two major drawbacks:
- The developer needs to actively decide which conventions each
DateFormatter
will support and. - The developer needs to take on the responsibility of ensuring that any date and time formatting rules are correct for each supported set of conventions.
In order to define the formatting rules for each dateFormat
value, the developer would need to answer questions such as:
- Which calendar to use?
- Which clock to use: 24-hour or 12-hour?
- What is the ordering of date elements?
- What is the date separator character(s)?
And the list goes on.
Answering all these questions correctly is a massive task and as it turns out, a totally unnecessary one as iOS already answers them (and many more besides) for us. In iOS, the linguistic, cultural and technological conventions are collected within the Locale
class (each locale's conventions are provided by the Unicode Common Locale Data Repository (CLDR) project). The conventions defined within each locale should match our user's expectations for how their world is measured and represented; as such Locale
plays a vital role in making our users comfortable when using our apps to complete their tasks. By working with the user's locale conventions, it's possible to produce formatted dates that match our user's expectations. Even better than just improving our user's in-app experience, is that this can all be achieved without actually having to know or care what those locale conventions actually are.
While each locale comes with default conventions, some of them can be customised by the user e.g. switching from 12-hour to 24-hour clock representation. So even if two users have the same locale, it would be a mistake to assume these locales where identical.
For the rest of this post I'm going to assume your locale is the default
US
locale. To keep the examples comparable, each will use a date based off of the Unix epoch timestamp:1165071389
which equates to2nd of December, 2006 at 14:56:29
. You can see the completed playground with all of the below examples by following this link.
Staying local
When it comes to presenting date information to the user, it's best to leave all locale concerns to DateFormatter
- there are two ways to do this:
- Using
DateFormatter.Style
- Using a template
Let's explore both these approaches in more detail.
Using DateFormatter.Style
One way to ensure that a Date
is always shown to the user using a formatting style that they are expecting is to use the DateFormatter.Style
enum. The DateFormatter.Style
enum is a set of predefined values that correspond to a date format, at the time of writing (iOS 12) this enum has 5 possible cases to choose from:
none
short
medium
long
full
Each case will take into account the user's locale settings when formatting dates. DateFormatter
has two DateFormatter.Style
properties: dateStyle
and timeStyle
. Each property works semi-independently of the other and as such each can take a different value - this flexibility allows for a wide range of formatting options (25 possible permutations):
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .full
dateFormatter.timeStyle = .medium
let formattedDate = dateFormatter.string(from: date)
formattedDate
would be set to Saturday, December 2, 2006 at 2:56:29 PM
.
In the above example, we can see the user's locale influencing the formattedDate
value in a number of ways:
- The date format ordering -
EEEE, MMMM d, y 'at' h:m:s a
. - The names of the months and days.
- The separator between the various date elements -
/
for calendar date elements and:
for time date elements.
It's also interesting to note that DateFormatter
is handling combining the calendar date and time elements together into a sentence structure that is appropriate for both DateFormatter.Style
values. So in the above example at
is being used as a connector between those two elements however if dateStyle
was changed to .short
then at
becomes ,
and we get 12/2/06, 2:56:29 PM
as the formatted date should be shown in a more compacted form - pretty powerful stuff 🤯.
To demonstrate the power of this further, if we set the locale
to Locale(identifier: "de_DE")
the above .full
example becomes Samstag, 2. Dezember 2006 um 14:56:29
and the .short
example becomes 02.12.06, 14:56:29
- all this localisation without me having to know anything about German date conventions or even anything about the German language - 🤯 in 🇩🇪.
Another way to use the DateFormatter.Style
approach is by using the available class method:
let formattedDate = DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .medium)
formattedDate
has the same value as the property based approach: 12/2/06, 2:56:29 PM
.
When deciding which option to use I tend to favour the property(s) approach when I either need to cache the DateFormatter
instance being used (see "Sneaky date formatters exposing more than you think" for more details on how to cache date formatters) or further customise it (outside of just the dateStyle
and/or timeStyle
properties). For all other scenarios, I favour the class method approach as I think it's easier to reason about and reads better (from the method name I know that I'm getting a localised string value back).
This is a helpful cheatsheet showing the output for each
DateFormatter.Style
case. To see the conventions used in different locales set thelocale
property for theDateFormatter
instance to the locale you are interested in e.g.dateFormatter.locale = Locale(identifier: "en_CA")
.
Using DateFormatter.Style
works really well if you to want to display a date in one of the available formats but if you want to display a custom format you need to go down a different path.
Using a template
A template
is a customised instruction to DateFormatter
of the date elements that the formatted date should contain - these date elements are specified using the same symbols as used with a fixed string date format approach: d
, MM
, y
etc. The beauty of the template
approach is that it will take this customised instruction and produce a formatted date that takes into account the user's locale when displaying those elements. For example to produce a formatted date only containing the day and month, the template could look like:
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("d MM")
let formattedDate = dateFormatter.string(from: date)
formattedDate
would be set to 12/2
. The more eagle-eyed among you will have spotted that two transformations have occurred here, the first is that a locale-specific separator was added between the date elements and that the date elements have switched positions from the ordering in the template i.e. from d MM
to MM d
🚀.
It's important to note that when defining the above template I added a space between the different date elements, strictly speaking this space wasn't needed -
dMM
would have resulted in the sameformattedDate
value. I included the space here only to make the template easier to read.
Just like with the DateFormatter.Style
, the template approach has both an instance and class interface:
let localisedDateFormat = DateFormatter.dateFormat(fromTemplate: "d MM", options: 0, locale: Locale.current)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = localisedDateFormat
let formattedDate = dateFormatter.string(from: date)
Again, formattedDate
would be set to 12/2
. The class approach is little more verbose than the instance approach but could be more useful if you wanted to pass the localised formatting string around rather a DateFormatter
instance.
Going remote
You may be thinking at this point:
"Is using dateFormat
with a fixed string ever the correct approach?"
The simple answer:
Yes.
The date formatting examples shown so far have been concerned with presenting a formatted date to the user. However as iOS developers we (often) have another consumer of our date data: the backend. Typically the backend will be expecting all date values to be sent using a fixed, locale-neutral format. In order to ensure that the user's locale does not affect these dates when converting from a Date
instance to the formatted date value we need to take full control of our DateFormatter
instances and hardcode the locale
, timeZone
and dateFormat
properties to match the backend's expectations:
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(identifier: "UTC")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
let formattedDate = dateFormatter.string(from: date)
formattedDate
would be set to 2006-12-02T14:56:29Z
regardless of the user's locale settings. An interesting side effect of setting the locale
property is that the formatter's calendar
property is always set to the default calendar for that locale (which for this local is the Gregorian calendar). It's important to note that en_US_POSIX
is not the same locale as en_US
- en_US_POSIX
while based off of en_US
is a special locale that isn't tied to any country/region so shouldn't change even if en_US
does change.
You may have noticed that the
dateFormatter
is using the ISO 8601 string format, in this case (and if your project has a base iOS version of at least iOS 10) I would recommend usingISO8601DateFormatter
instead of the more genericDateFormatter
class.
Letting go of (some) control
When displaying a date in our apps, it makes sense to do so in a format that the user is expecting and can easily understand- this isn't as simple as it first seems. Thankfully, DateFormatter
has two very straight forward ways to achieve this:
- Using
DateFormatter.Style
- Using a template
Both approaches work well and require very little effort to solve what is a real iceberg of a problem. The only thing it costs us is that we need to give up a little bit of control on exactly how the date is formatted - that's a price I'm happy to pay to ensure that my users are shown dates in the most convenient format for them and I don't need to think about which calendar a certain locale uses or if month comes before day, etc.
Now I just need to convince those pixel-perfect designers on my team that giving up some control is actually a good thing... 😅
If you are interested, there is an accompanying playground that contains all of the above code snippets.
What do you think? Let me know by getting in touch on Twitter - @wibosco