Articles, podcasts and news about Swift development, by John Sundell.

Exploring some of the lesser-known, built-in Formatter types

Published on 02 Apr 2021

Generating formatted string representations of various kinds of values can often be quite tricky, especially if we want those strings to be fully adapted to the user’s language, locale and other system-wide preferences.

Thankfully, Apple ships a quite comprehensive suite of fully localized formatters as part of each of their platforms, and while we’ve already taken a look at the most commonly used among those — like DateFormatter and NumberFormatter — in this article, let’s instead explore some of the lesser-known Formatter subclasses, and how they can prove to be incredibly useful in certain situations.

Names of people

If there’s one thing that all Formatter types that come built into the system have in common is that they’re all performing tasks that are much more complicated than what they initially might seem like.

Take user names, or names of people in general, as an example. Let’s say that we’re working on an app that stores each user’s full name using a single fullName property defined within a User data model — like this:

struct User {
    var fullName: String
    ...
}

Then, let’s say that we’re currently making the assumption that if we split a given user’s fullName string into space-separated components, then the first component will always be the user’s “first name” — which we then use as that user’s “display name” across our app’s UI:

extension User {
    func displayName() -> String {
        String(fullName.split(separator: " ")[0])
    }
}

The above is an incredibly common mistake to make. Not only does the preferred order of a name’s various components differ between different languages, countries, and cultures around the world — names can also often have many more components than just “first name” and “last name”, which our code currently doesn’t account for.

For example, what if a user entered “Dr. John Appleseed” as their full name? Then that user’s display name would currently be computed as “Dr.”. Not great.

Thankfully, this is a problem that our friends at Apple have already solved. Enter PersonNameComponentsFormatter, which handles all of the complexities involved in correctly parsing and formatting a person’s name. Here’s how we could update our displayName method to use that formatter instead:

extension User {
    func displayName() -> String {
        let formatter = PersonNameComponentsFormatter()
        let components = formatter.personNameComponents(from: fullName)
        return components?.givenName ?? name
    }
}

Not only will the above new version yield results that are much more correct regardless of the user’s name or locale, but the intent of our code is now arguably much more clear as well.

PersonNameComponentsFormatter also enables us to turn a set of name components into a fully localized name as well, which can be incredibly useful when dealing with user names that are already split up into separate components — like this:

struct User {
    var firstName: String
    var lastName: String
    ...
}

extension User {
    func fullName() -> String {
        let formatter = PersonNameComponentsFormatter()

        var components = PersonNameComponents()
        components.givenName = firstName
        components.familyName = lastName

        return formatter.string(from: components)
    }
}

The reason that we’ve implemented all of our name computation code as methods, rather than computed properties, is to clearly show that our logic requires some amount of processing, since it’s not just returning a value that can be computed in O(1) time. To learn more about that approach, check out “Computed properties in Swift”.

Addresses

Next, let’s take a look at a Formatter type that ships as part of the Contacts framework — CNPostalAddressFormatter, which provides an easy way to format addresses into localized strings.

Just like names, the way addresses are supposed to be formatted varies a lot between different countries, and handling all of that complexity ourselves would be quite overwhelming.

As an example, let’s say that we’re working on some form of shopping app that contains the following ShippingAddress type:

struct ShippingAddress {
    var street: String
    var postalCode: String
    var city: String
    var country: String
}

If we now wanted to generate a formatted string representation of such an address (for example to show the user what address that a given product will be shipped to), then all that we have to do is to convert our shipping address data into a CNPostalAddress instance (which can be done using its mutable counterpart, CNMutablePostalAddress), and then ask CNPostalAddressFormatter to convert that instance into a string — like this:

import Contacts

extension ShippingAddress {
    func formattedString() -> String {
        let formatter = CNPostalAddressFormatter()

        let address = CNMutablePostalAddress()
        address.street = street
        address.postalCode = postalCode
        address.city = city
        address.country = country

        return formatter.string(from: address)
    }
}

Of course, if we’re dealing with addresses retrieved using the Contacts framework itself, then those will already be represented using CNPostalAddress instances, so the above type conversion is only necessary when working with a custom way of storing addresses.

Relative time

Although the standard DateFormatter type is quite well-known and commonly used within all sorts of applications, it also has a somewhat lesser-known “cousin” called RelativeDateTimeFormatter which can be used to generate localized strings that describe the relative time interval between two dates.

For example, here’s how we could use that specialized formatter to compute a string that tells the user how much time that’s left until a given Event starts:

struct Event {
    var title: String
    var startDate: Date
    ...
}

extension Event {
    func relativeTimeString() -> String {
        let formatter = RelativeDateTimeFormatter()
        formatter.dateTimeStyle = .named
        formatter.formattingContext = .beginningOfSentence
        return formatter.localizedString(for: startDate, relativeTo: Date())
    }
}

What’s really cool is that by specifying that we want to use the named date time style (like we do above), words like “Tomorrow” and “Yesterday” will be used to describe intervals that have specific names.

Lists

Finally, let’s take a quick look at ListFormatter, which perhaps isn’t as complex as some of the other formatters that we’ve explored so far, but it can still be useful in certain situations.

Essentially, ListFormatter enables us to concatenate an array of strings into a single, localized list-like string. Using it is as easy as passing the array of strings that we wish to join to its static localizedString method, and it’ll then return a formatted string based on the user’s locale and language settings:

// In English:

let ingredientsList = ListFormatter.localizedString(
    byJoining: ["Apples", "Sugar", "Flour", "Butter"]
)

print(ingredientsList) // "Apples, Sugar, Flour, and Butter"

// In Swedish:

let ingredientsList = ListFormatter.localizedString(
    byJoining: ["Äpplen", "Socker", "Mjöl", "Smör"]
)

print(ingredientsList) // "Äpplen, Socker, Mjöl och Smör"

Note how the English and Swedish versions doesn’t just differ in terms of language, but also in how commas are used. Those kinds of details might seem insignificant, but can really make an app feel much more thoroughly localized — and once again, we didn’t have to write any custom code to account for those variances — it’s all handled for us by the system.

Conclusion

It’s very clear that Apple’s dedication to high-quality localization isn’t just limited to their own apps and system features — their various platform SDKs also ship with numerous APIs, tools and features that we can use to elevate the localization of our own apps as well. All that we have to do is to use the right APIs when dealing with locale-dependent values — such as names, numbers, addresses and dates.

Hopefully this article has given you a few insights on how to do just that, and feel free to reach out via either Twitter or email if you have any questions or comments.

Thanks for reading!