Streaming is available in most browsers,
and in the Developer app.
-
Formatters: Make data human-friendly
Save yourself time and frustration: When you display data in your app — including dates, times, measurements, names, lists, numbers, or strings — learn how to format it correctly and provide a great experience. We'll walk you through the Formatter APIs as well as how SwiftUI works with stringsdict, and show you how they can help do the heavy lifting of formatting data. Learn about best practices and how to avoid common mistakes.
Resources
- Displaying Human-Friendly Content
- Expanding Your App to New Markets
- Unicode Date Field Symbol Table
- Xcode Help: Add language plural variants
Related Videos
WWDC22
WWDC21
WWDC20
-
Download
Hello and welcome to WWDC.
Hello, everyone. My name is Karan Miśra, and today I'd like to talk about how we can format data in our apps to be friendly and understandable.
Every day, we use many apps which shows us all different kinds of data. Some apps, like Weather, are full of different measurements.
Others, like Health, show us some key statistics with trends.
And yet others, like Notes, may only show us a simple date or time stamp. No matter how prominently the data is shown, it's important to get it right all the way from what units are used to what formats are used. And this is crucially important regardless of whether the app is available in just one language or whether it's available in dozens of different languages.
We also update the underlying algorithms and data sets that power our formatters, and one of our focus areas has been expanding our support for combinations of languages and regions. Why? Because we're living in an increasingly globalized world and we find that people all over the world are using their devices in languages that may not be traditionally associated with that region.
For example, someone living in Abu Dhabi might be using their iPhone in French.
Before, this meant that we would get number formats that were intended for France with the decimal separator as a comma, as shown here in the Home App.
Now with iOS 14, and the latest versions of Mac OS, watchOS and tvOS, we will get formats that are better suited for the region, which is UAE in this case. And it goes without saying that this change is not limited to a single language or region but is a general algorithm that will improve the formats across hundreds of combinations of languages and regions.
I want to show you the power of using the formatter APIs in your app, how much time and effort these APIs can save you, and how you can use these APIs to build versatile UIs.
I'm going to walk through several sections, and as I walk through each of them, I will focus on real-world examples from Apple's own apps, going down in order from most specific to most generic. My goal is to show that we have an extensive set of APIs at our disposal, that simple things are easy and complex things are possible.
Let's get started with dates and times.
Let's start with a simple example. In the Notes app, at the top of every note, you will see the last modification date and time.
Let's see how we can do this.
Here we want to show a full date with day, month and year and also the time. This is something that can be accomplished with the predefined styles in dateFormatter, with the medium style for the date and the short style for the time.
Note that the word "at" is automatically added here as a consequence of this being a medium-length format. So, as I said, simple things are easy.
Now let me show you that more complex things are possible too. Here, we have the Screen Time UI in Settings and we want to show the day of the week followed by the day and month but not the year. This is not something that any of the predefined styles can offer you, but it is possible by specifying a template.
As one more example from Screen Time, which you will see in many other UIs too, if you just want the abbreviations for the days of the week, you can request that with a custom template. As we can see, the template mechanism is really powerful. So let's dive a little deeper into templates.
Unicode publishes various technical reports, including this one on dates, one of a seven-part series, in fact, for which I'm proud to say that Apple, as a Unicode member, has been one of the key contributors. This is an excellent reference for templates, and there are lots of explanations and examples. So when I was looking for day of the week earlier, I scrolled down, and found this section. Here you can see that I can choose between an uppercase "E," a lowercase "e" and a lowercase "c." So which one should I use? Now, as you may recall, I was showing these days of the week labels in a graph, so these are stand-alone and not part of a longer format. This helps me choose the lowercase "c" for my template.
Now that we've chosen a template, let's see how it works.
Here, you're seeing the output of different lengths of lowercase "c." Notice that here the template with five C's is the one that's appropriate, because we want the most abbreviated form. Also, note the way that these lengths are interpreted across different languages will vary. Here, we see that in English each of the five lengths produces a different output but not so in Arabic or Japanese. So it's important to choose the right template and consider all the locales that we're supporting, since some distinctions may only manifest for certain languages or locales.
This is also why it was very important that we chose the stand-alone style. It may not make a difference in English, but it may make a difference for other languages.
Another important thing to note about templates is that the order of the fields does not matter. This template is the same as this template which is the same as this template.
The template takes the pieces of data that we're interested in and it is the formatter's job to assemble them into something that makes sense for the locale.
So for American English, you'll see that it adds a comma.
For British English, you'll see that it makes sure that the month follows the day and not the other way around.
And as one last example, you'll see that for Japanese it adds the characters for month and day which are needed to produce a sensible-looking date in Japanese.
Lastly, I should point out that dateFormatter has a property called dateFormat, and we should never set the template directly onto that property, since it will cause it to be interpreted literally and almost certainly produce the wrong result. Instead, we should always create our date formats from a template first.
There are a few more APIs for formatting dates and times.
DateComponentsFormatter helps you format components, such as those used in durations.
You can also format ranges of time, like this example from the Health app by using DateIntervalFormatter.
It even takes care of avoiding repeating elements that are both in the start and end date.
Lastly, the RelativeDateTimeFormatter is a powerful tool for dates in the past or future which you want to present in a natural manner.
Next up, let's take a look at measurements.
If your app uses any kind of measurements, MeasurementFormatter is your friend. Let me show you a few examples that we use in the Weather app.
Let's start with temperature. Note that in all these examples, I am setting the measurements in metric units, but the display you see on the right has everything localized for the US, which uses imperial units. So the temperature of 16 degrees Celsius will come out as 61 degrees Fahrenheit. The same goes for speed. Here the value is converted to miles per hour for the US.
And similarly for pressure. I'm showing these examples because your data source for measurements may use metric or other units and MeasurementFormatter does all the heavy lifting for you, not only across units but also across languages and locales. Now, while I just showed you examples of three different kinds of measurements, MeasurementFormatter supports many, many more. And, in case it doesn't support a unit that you'd like to use, it can even support custom units. For all this and more, do check out the excellent WWDC 2016 talk on "Measurements and Units." Next, let's talk about names.
Names are one of the most personal bits of data that we display in our apps, and we all have a very strong response when we see our names on screen.
So getting name formatting right is absolutely crucial to making a good impression.
Luckily, PersonNameComponentsFormatter, which I like to call "Name Formatter" for short, makes it easy to do the right thing.
All we need to do is to populate the components object and ask for the formatted strings.
By default, Name Formatter will choose the medium style which includes all the primary components like family name and given name.
We can also request a short style, which, depending on user preferences, may use the nickname or a shortened version of the name.
The abbreviated style is intended for specific use cases like monograms, as shown here. Over the past few years, more and more apps have been using monograms. So let's dive a little deeper into them.
Monograms are a great alternative in the absence of an avatar or photo while still making the UI feel friendly and inviting. And the Name Formatter was designed with monograms in mind. However, it is important to note that monograms cannot be generated for all names and some abbreviated names may be too long to fit in a given UI.
With Swift, it's easy to restrict monograms based on a length check. Because Swift's count is based on user-visible characters and not Unicode code points, this works well across languages.
Of course, a character count cannot determine how tall or wide the string would be when rendered. So we still need to make sure that the string fits appropriately.
When the character count is too long or the string is too large to fit, a fallback option needs to be used, such as a generic icon that can be used in place of a monogram.
Lastly, it's worth mentioning that the Name Formatter has a lot of intelligence built in and uses several pieces of information when formatting names, such as the user's language settings, their contact settings, such as name order, as well as the name itself.
For example, here a Japanese name is shown on an iPhone running in English. I've intentionally chosen this example to show you that the Name Formatter will render the name appropriately in all cases, even in cases where the language of the name is different from the iPhone language.
For names written in Chinese, Japanese and Korean, this means that they will always use the family name followed by given name, which is the only appropriate way to write these names.
Next up, let's see how we can make well-formatted lists. Anytime we're showing a list of items that's formatted as human readable text, we can use ListFormatter. Let me show you how it can not only save us time but also make our app look polished by getting the nuances right.
The API couldn't be easier. We simply specify an array of strings or other objects that we want to combine into a list, and we get back a concatenated list.
Let's see the subtle benefits we get by doing this. This is the keyboard settings UI, and here we're formatting a very simple list with two elements: the language names "English" and "Spanish." In English, all this takes is inserting the word "and" in the middle.
And if the order of the languages is changed, it's still the same.
But if we take a look at the same UI now localized to Spanish, we'll see that the word for "and" is in Spanish, but did you know that the Spanish word for "and" can vary based on context? Notice that when the order of the elements is changed, so is the Spanish word for "and." In fact, in iOS 14, as well as the latest versions of macOS, tvOS and watchOS, ListFormatter has been updated to adhere to the grammatical rules of several languages, including Spanish.
This is one of the many reasons that we should use formatter APIs wherever possible. Not only do they abstract away a ton of complexity and provide localization for free, as the underlying implementation is updated and improved, our app gets improved behavior without modifying a single line of code. Now let's talk about two generic data types. Let's start with numbers. Here is a quick quiz. What's the number you're seeing onscreen? It's a little bit of a trick question. Depending on what language we use and what region we're in, we may interpret this as 32,768 or as 32.768. In this case, the decimal separator button on the calculator gives it away and tells us that this is actually 32.768.
And this is how we can format it super easy.
Now what happens if the app is instead launched with the language set to Arabic and the region to Egypt? As we see here, NumberFormatter does all the heavy lifting to ensure that everything is localized appropriately.
It's also worth noting that if we need to access certain symbols that change according to the language or locale, there are convenience methods to fetch the percentSymbol and the decimalSeparator. In fact, there are also convenience APIs for many more symbols that aren't shown here.
NumberFormatter supports several different styles, including a percent style. And, as we would expect, it handles nuances across languages and regions here too.
For example, we can see that it correctly formats the number with the percent sign going in front of the number for Turkish.
I'm not going to go into more detail on NumberFormatter here because it is chock-full of useful functionality, but please do check out the documentation, or try using it in an Xcode Playground. Lastly, let's talk about strings.
While the various formatter APIs will cover a wide range of data types that are commonly formatted across all apps, your app may have its own kinds of data that don't have a standard formatter. For example, there's no standard formatter that will format the number of photos. Let's see an example of how this would work.
Doing this is extremely simple. Using the text element in SwiftUI, we specify the text as we normally do. As far as code goes, that's all we need to write.
Then all we need to do is to make sure that we have a stringsdict file that contains the corresponding string.
To learn more about stringsdict, or even more about localization in general, do take a look at the documentation at the URL mentioned here. Let's say our translators have translated the text into English and Arabic. Let's see what the result looks like. When a single photo is selected, note that while the English uses the numeral "1," in Arabic, it spells out the words, as that is more natural.
When two photos are selected, the English, again, uses a number, but "photo" becomes "photos," following English's grammar. On the other hand, in Arabic, the dual form is used following Arabic grammar.
For three photos, both English and Arabic look similar, using the number followed by the plural form of the word "photo." And this goes on for numbers up to ten. For 11 photos or more, Arabic grammar dictates that a different plural form be used. Now, maybe you're interested in learning the grammar rules of a hundred languages. I know I am. However, what I want you to take away from this is that if you correctly localize strings using stringsdict, and if you use the formatters wherever you can in your app, your app will shine regardless of whether you support only one language or a hundred. Also, all of this is code that you need to write only once, no matter how many languages you support in your app, either now or in the future.
Lastly, to show you even more features of all the formatters I talked about and let you explore, we have a sample app that you can download and play with. So happy formatting.
-
-
2:25 - Dates and times
// Dates and Times // Date with Day/Month/Year and Time let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium dateFormatter.timeStyle = .short dateFormatter.string(from: Date()) // Day of Week + Date + Month let dateFormatter = DateFormatter() dateFormatter.setLocalizedDateFormatFromTemplate ("MMMMdEEEE") dateFormatter.string(from: Date()) // Abbreviated Day of Week let dateFormatter = DateFormatter() dateFormatter.setLocalizedDateFormatFromTemplate ("ccccc") dateFormatter.string(from: Date())
-
5:56 - Date components formatter
// Dates and Times // Date and Time Components let formatter = DateComponentsFormatter() formatter.unitsStyle = .abbreviated let components = DateComponents(hour: 2, minute: 26) formatter.string(from: components) // Date and Time Intervals let formatter = DateIntervalFormatter() formatter.dateTemplate = "dMMM" formatter.string(from: startDate, to: endDate) // Relative Dates and Times let formatter = RelativeDateTimeFormatter() formatter.dateTimeStyle = .named formatter.localizedString(from: DateComponents(day: -1))
-
6:29 - Measurements
// Measurements // Temperature let formatter = MeasurementFormatter() let temperature = Measurement<UnitTemperature> (value: 16, unit: .celsius) formatter.numberFormatter.maximumFractionDigits = 0 formatter.string(from: temperature) // Speed let speed = Measurement<UnitSpeed> (value: 14, unit: .kilometersPerHour) formatter.string(from: speed) // Pressure let pressure = Measurement<UnitPressure> (value: 1.01885, unit: .bars) formatter.string(from: pressure)
-
7:49 - Names
// Names let formatter = PersonNameComponentsFormatter() var nameComponents = PersonNameComponents() nameComponents.familyName = "Iwasaki" nameComponents.givenName = "Akiya" nameComponents.nickname = "Aki-chan" // Full Name formatter.string(from: nameComponents) // Short Name: Respects User Preferences formatter.style = .short formatter.string(from: nameComponents) // Abbreviated Name formatter.style = .abbreviated formatter.string(from: nameComponents)
-
8:31 - Abbreviated name (monogram)
// Abbreviated Name: Monogram formatter.style = .abbreviated let monogram = formatter.string(from: nameComponents) if (monogram.count <= 2) { // Use Monogram } else { // Use Icon }
-
9:23 - Name formatter
// Names let formatter = PersonNameComponentsFormatter() var nameComponents = PersonNameComponents() nameComponents.familyName = "岩崎" nameComponents.givenName = "晃也" nameComponents.nickname = "あきちゃん" // Full Name formatter.string(from: nameComponents) // Short Name: Respects User Preferences formatter.style = .short formatter.string(from: nameComponents) // Abbreviated Name formatter.style = .abbreviated formatter.string(from: nameComponents)
-
10:15 - Lists
// Lists // English Localization let items = [ "English", "French", "Spanish" ] ListFormatter.localizedString(byJoining: items) let items = [ "English", "Spanish" ] ListFormatter.localizedString(byJoining: items) let items = [ "Spanish", "English" ] ListFormatter.localizedString(byJoining: items) // Spanish Localization let items = [ "Inglés", "Español" ] ListFormatter.localizedString(byJoining: items) let items = [ "Español", "Inglés" ] ListFormatter.localizedString(byJoining: items)
-
12:01 - Numbers
// Numbers let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.string(from: 32.768) // French (France) let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.string(from: 32.768) // Arabic (Egypt) formatter.percentSymbol formatter.decimalSeparator
-
12:33 - Numbers formatter
// Numbers let formatter = NumberFormatter() formatter.numberStyle = .percent formatter.string(from: 0.71) // English (US) let formatter = NumberFormatter() formatter.numberStyle = .percent formatter.string(from: 0.71) // Turkish (Turkey)
-
13:24 - Strings
// Strings var body: some View { Text("\(photosCount) Photos Selected") }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.