David Yang

Tips and posts for iOS developers from an iOS developer.

At WWDC19, Apple introduced two new formatters that will help developers in many ways. Only available from iOS 13+ (beta at the time this article is being written).

RelativeDateTimeFormatter

RelativeDateTimeFormatter will return a localized string from a Date relatively to another.

A sample code is much more easier to understand:

let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named

let now = Date()
formatter.localizedString(for: now, relativeTo: now)
// output: "now"

let tenSeconds = Date(timeInterval: 10, since: now)
formatter.localizedString(for: tenSeconds, relativeTo: now)
// output: "in 10 seconds"

let yesterday = Date(timeInterval: -24 * 60 * 60, since: now)
formatter.localizedString(for: yesterday, relativeTo: now)
// output: "yesterday"

let lastWeek = Date(timeInterval: -7 * 24 * 60 * 60, since: now)
formatter.localizedString(for: lastWeek, relativeTo: now)
// output: "last week"

let tomorrow = Date(timeInterval: 24 * 60 * 60, since: now)
formatter.localizedString(for: tomorrow, relativeTo: now)
// output: "tomorrow"

let twoHours = Date(timeInterval: 2 * 60 * 60, since: now)
formatter.localizedString(for: twoHours, relativeTo: now)
// output: "in 2 hours"

A .numeric date time style is also available, providing the following results.

let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .numeric

let now = Date()
formatter.localizedString(for: now, relativeTo: now)
// output: "in 0 seconds"

let tenSeconds = Date(timeInterval: 10, since: now)
formatter.localizedString(for: tenSeconds, relativeTo: now)
// output: "in 10 seconds"

let yesterday = Date(timeInterval: -24 * 60 * 60, since: now)
formatter.localizedString(for: yesterday, relativeTo: now)
// output: "1 day ago"

let lastWeek = Date(timeInterval: -7 * 24 * 60 * 60, since: now)
formatter.localizedString(for: lastWeek, relativeTo: now)
// output: "1 week ago"

let tomorrow = Date(timeInterval: 24 * 60 * 60, since: now)
formatter.localizedString(for: tomorrow, relativeTo: now)
// output: "in 1 day"

let twoHours = Date(timeInterval: 2 * 60 * 60, since: now)
formatter.localizedString(for: twoHours, relativeTo: now)
// output: "in 2 hours"

The RelativeDateTimeFormatter also supports localization. By default, it will use the current locale, but it can be overriden with the locale property.

let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named

let now = Date()
formatter.locale = Locale(identifier: "fr_FR")
formatter.localizedString(for: now, relativeTo: now)
// output: "maintenant"
formatter.locale = Locale(identifier: "en_US")
formatter.localizedString(for: now, relativeTo: now)
// output: "now"

ListFormatter

ListFormatter is an easy to use formatter that will simply allow us to get a displayable and localized string from an array of item.

let formatter = ListFormatter()
formatter.locale = Locale(identifier: "en_US")

let justiceLeaguePeople = ["Batman", "Superman", "The Flash", "Cyborg", "Wonder Woman", "Aquaman"]
formatter.string(from: justiceLeaguePeople)
// output: "Batman, Superman, The Flash, Cyborg, Wonder Woman and Aquaman"

We can also get more complex results if working with some any other types of element.

Let’s see how it works with the following Hero struct. To make it simple, my Hero will be made of 3 properties: a firstname, a lastname and a “made-up name” like Spider-Man likes to call it.

struct Hero {
    let firstname: String
    let lastname: String
    let madeupName: String
}

First of all, I need to write a custom Formatter for my Hero struct.

class HeroFormatter: Formatter {
    enum FormatStyle {
        case civil, madeup
    }

    var formatStyle: FormatStyle = .civil

    override func string(for obj: Any?) -> String? {
        guard let hero = obj as? Hero else { return nil }
        switch formatStyle {
        case .madeup:
            return hero.madeupName
        case .civil:
            return "\(hero.firstname) \(hero.lastname)"
        }
    }
}

Pretty straightforward, by default my HeroFormatter will use my custom formatStyle .civil which will simply return a concatenation of the lastname and firstname properties as the formatted string output.

Let’s build our list of heroes.

let avengers = [
    Hero(firstname: "Tony", lastname: "Stark", madeupName: "Iron Man"),
    Hero(firstname: "Steve", lastname: "Rogers", madeupName: "Captain America"),
    Hero(firstname: "Natasha", lastname: "Romanov", madeupName: "Black Widow"),
    Hero(firstname: "Bruce", lastname: "Banner", madeupName: "The Hulk"),
    Hero(firstname: "Clint", lastname: "Barton", madeupName: "Hawkeye"),
    Hero(firstname: "Thor", lastname: "Odinson", madeupName: "Thor")
]

Now we can use the ListFormatter and inject our HeroFormatter into it.

let civilNameFormatter = HeroFormatter()
civilNameFormatter.formatStyle = .civil

let formatter = ListFormatter()
formatter.itemFormatter = civilNameFormatter
formatter.string(from: avengers)

// output: "Tony Stark, Steve Rogers, Natasha Romanov, Bruce Banner, Clint Barton and Thor Odinson"

But something’s wrong, super-heroes have secret identities…

Spider-Man to Dr. Strange

So let’s use the .madeup format style. We will get the following.

let madeupNameFormatter = HeroFormatter()
madeupNameFormatter.formatStyle = .madeup

let formatter = ListFormatter()
formatter.itemFormatter = madeupNameFormatter
formatter.string(from: avengers)

// output: "Iron Man, Captain America, Black Widow, The Hulk, Hawkeye and Thor"

Of course, just like any other native Formatters, ListFormatter also supports localization.

Conclusion

There is no doubt those new formatters will be useful. The documentation is available on Apple’s Documentation website.

Links: https://developer.apple.com/documentation/foundation/relativedatetimeformatter https://developer.apple.com/documentation/foundation/listformatter

By the way, who cares about secret identities anyway?

I am Iron Man