Formatters: Make data human-friendly

Description: 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.

Overview

This talk is about formatting data to make it friendly and understandable by humans.

Some apps, like Weather, are full of different measurements. Others, like Health, show key statistics with trends. And yet others, like Notes, only show a simple date or timestamp.

It's important to get data, formats, and measurements correct in multiple languages.

Apple is finding more language and region combinations. For example, someone living in Abu Dhabi might be using their iPhone in French. iOS 14 identifies some of these new combinations and adapts accordingly.

The Formatter APIs can save you time when formatting:

  1. Dates and times
  2. Measurements
  3. Names
  4. Lists
  5. Numbers
  6. Strings

1. Dates and Times

In the Notes app, the top of each note shows the time it was last modified. You can get a string that says "June 22, 2020 at 9:41 AM" with the following code:

// Date with Day/Month/Year and Time
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium  // This automatically adds "at"
dateFormatter.timeStyle = .short
dateFormatter.string(from: Date())

In the Settings app, the screen time for a day is shown as "Friday, June 5". The string is generated by:

// Day of Week + Date + Month, but do not show Year.
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("MMMMdEEEE")
dateFormatter.string(from: Date())

Another example from screen time is the abbreviated day of the week ("M", "T", "W", etc.):

// Abbreviated Day of Week
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("ccccc")
dateFormatter.string(from: Date())

Templates

The second two examples above used templates to format dates and times in a certain way. Unicode publishes several technical reports on dates. The Date Field Symbol Table describes many different date formatting styles which you can pass to DateFormatter.

These templates produce different output in different languages. Here's a table comparing English, Arabic, and Japanese:

It's important to choose a template and consider how data will be shown in all of your app's supported languages.

One thing to note about templates is that the order of fields does not matter. "dMMMEEE" == "MMMdEEEE" == "EEEEMMMd". It's the formatter's job to assemble the pieces into something that makes sense for each locale. Here's how this template string is evaluated in different locales:

Lastly, be sure to never set the template directly on dateFormat. Instead, use the dateFormat(fromTemplate:) function.

Some more APIs for Dates and Times

DateComponentsFormatter helps you format date components, such as those used in durations.

// Date and Time Components
// This produces "2h 26m"
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
let components = DateComponents(hour: 2, minute: 26)
formatter.string(from: components)

You can also format ranges of time by using DateIntervalFormatter. This also takes care of repeating elements that are both in the start and end date. (Note: This is what the presenter said verbatim. I assume this means it won't "double-count" dates in an interval. I have searched the API, the source code, and some related blog posts but there's no mention of repeating elements).

// Date and Time Intervals
// This produces something like "May 31 - June 6"
let formatter = DateIntervalFormatter()
formatter.dateTemplate = "dMMM"
formatter.string(from: startDate, to: endDate)

You can also show dates in the past or future relative to the present:

// Relative Dates and Times
// This produces something like "Yesterday, June 22"
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
formatter.localizedStringg(from: DateComponents(day: -1))

2. Measurements

If your app has any kind of measurements, MeasurementFormatter is your friend!

Note that all these measurements use metric units, but in the US locale, these measurements will be converted to Fahrenheit, miles per hour, and inches of mercury, respectively.

// Temperature
// In US Locale, this produces "61°".
let formatter = MeasurementFormatter()
let temperature = Measurement<UnitTemperature>(value: 16, units: .celsius)
formatter.numberFormatter.maximumFractionDigits = 0
formatter.string(from: temperature)

// Speed
// In US Locale, this produces "9 mph".
let speed = Measurement<UnitSpeed>(value: 14, unit: .kilometersPerHour)
formatter.string(from: speed)

// Pressure
// In US Locale, this produces "30.09 inHg".
let pressure = Measurement<UnitPressure>(value: 1.01885, unit: .bars)
formatter.string(from: pressure)

MeasurementFormatter supports many different types of measurements. Here's a list:

You can also make your own custom units. For all this and more, see Measurements and Units.

3. Names

Names are one of the most personal bits of data that we display in our apps. Getting name formatting right is crucial to making a good impression. PersonNameComponentsFormatter makes it easy.

let formatter = PersonNameComponentsFormatter()
var nameComponents = PersonNameComponents()
nameComponents.familyName = "Iwasaki"
nameComponents.givenName = "Akiya"
nameComponents.nickname = "Aki-chan"

// Full name
// Produces "Akiya Iwasaki"
formatter.string(from: nameComponents)

// Short Name: Depends on User Preferences
// May use nickname or a shortened version of the name.
formatter.style = .short
formatter.string(from: nameComponents)

// Abbreviated Name
// Produces "AI" in the monogram.
formatter.style = .abbreviated
formatter.string(from: nameComponents)

Monograms

Monograms are a great alternative in the absence of an avatar or photo. They still make the UI feel friendly and inviting.

However, monograms cannot be generated for every name. Some monograms may be too long to fit in a given UI. Swift's count property is based on user-visible characters and not the number of Unicode code points, which makes a length check easy. A character count cannot determine how wide or tall the rendered string will be, so we still need to make sure the monogram fits appropriately.

Name Language vs Device Language

In the case where a person's name is in Japanese but the device language is English, PersonNameComponentsFormatter respects the Japanese style of putting the family name before the given name.

4. Lists

ListFormatter helps us make our lists look polished across locales.

We can provide an array of strings to ListFormatter.localizedString(byJoining:) to get back a human readable list in the target language.

let items = ["English", "French", "Spanish"]
ListFormatter.localizedString(byJoining: items)

In English, this produces "English, French, and Spanish". Let's give it 2 items instead:

let items = ["English", "Spanish"]
ListFormatter.localizedString(byJoining: items)

In English, this produces "English and Spanish". Notice how there are no commas anymore.

In Spanish, it's not as simple as inserting "and"; the word for "and" can be "y" or "e" depending on context.

// Spanish Localization
let items = ["Inglés", "Español"]
ListFormatter.localizedString(byJoining: items)
// "Produces Inglés y Español"

let items = ["Español", "Inglés"]
ListFormatter.localizedString(byJoining: items)
// "Produces Español e Inglés"

In iOS 14, as well as the latest versions of macOS, tvOS, etc., ListFormatter has been updated to adhere to the grammatical rules of several languages.

5. Numbers

In US English, "32,768" means "thirty-two thousand, seven-hundred and sixty-eight". In France, the comma is a decimal separator, so this number is "thirty-two point seven six eight".

That difference is why localization is important. Luckily, NumberFormatter does all the localization for you.

If you need to access certain symbols that can change by locale, there are convenience properties to access those symbols. For example, the property to access the percent symbol is formatter.percentSymbol.

NumberFormatter formats values like percentages differently depending on locale. For example, in US English, the percent symbol goes after the number, whereas in Turkish, the percent symbol goes first.

NumberFormatter is full of features. To see what it can do for you, read the documentation or try it out in an Xcode Playground.

6. Strings

Your app may have its own kind of data that doesn't have a standard formatter. For example, there is no standard formatter to show the number of photos.

To localize the string below, you need to have a stringsdict file.

"\(photosCount) Photos Selected"

The file should look something like this:

To learn more about stringsdict or more about localization in general, visit the URL in the slide above.

The more parts of your app you can localize, the more you can benefit from improvements to Apple's Formatters without changing a single line of code.

Sample App

To explore all the different types of formatters, there's a sample app you can download and play with in Xcode Playgrounds.

Missing anything? Corrections? Contributions are welcome 😃

Related