David Yang
Tips and posts for iOS developers from an iOS developer.
RelativeDateTimeFormatter and ListFormatter
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…
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?