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:
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
dateFormatvalue that's unique for each user's conventions.
- Move the
dateFormatvalue into the localisation
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
DateFormatterwill 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 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
USlocale. To keep the examples comparable, each will use a date based off of the Unix epoch timestamp:
1165071389which equates to
2nd of December, 2006 at 14:56:29. You can see the completed playground with all of the below examples by following this link.
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 a template
Let's explore both these approaches in more detail.
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:
Each case will take into account the user's locale settings when formatting dates.
DateFormatter has two
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
, 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(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
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.Stylecase. To see the conventions used in different locales set the
localeproperty for the
DateFormatterinstance to the locale you are interested in e.g.
dateFormatter.locale = Locale(identifier: "en_CA").
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
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:
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 -
dMMwould have resulted in the same
formattedDatevalue. 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)
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
You may be thinking at this point:
dateFormat with a fixed string ever the correct approach?"
The simple answer:
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
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_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
dateFormatteris 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 using
ISO8601DateFormatterinstead of the more generic
Letting go of a little bit of 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 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