Categories
iOS Development

How to use the RelativeDateTimeFormatter in Swift

Working with dates in Swift is a thing I remembered to struggle for a long time as a Junior developer. However, you should get used to objects that have date properties. E.g. you can sort stuff (e.g. see https://mic.st/blog/how-to-sort-an-array-of-objects-by-date-property/) easily by date. If you ever tried to develop your own date formatter, you will quickly run into a lot of pitfalls (Even before talking about localization😄). Here, I will give you an overview on how to use the RelativeDateTimeFormatter in Swift.

Currently, I am working on a private project that includes local notifications and a small dashboard that shows short information about these notifications. I wanted to make it friendly with something like “next notification in 18 hours”, etc.
So, how would you do that?
My first solution was to just present the notification’s trigger Date using the powerful and well known DateFormatter or DateComponentsFormatter plus some logic.
This is totally fine and might work. However, there is also a nice formatter for just this single use case. 🥳
Furthermore, one of the things I have learnt so far using Swift: If it’s already built-in, do not reeinvent it because your solution is most probably worse (especially when localization is involved 😄)

An image of a pocket watch laying in sand.
Formatting times in Swift can look as nice as this stock photo!

(I am not sure why Apple did not mention this nice formatter in the Date and Times overview at https://developer.apple.com/documentation/foundation/dates_and_times. However, it is mentioned on the general overview with all the formatters: https://developer.apple.com/documentation/foundation/data_formatting )

The formatter’s output

The documentation in the Obj-C header states pretty clear what you can expect from this formatter. It will give you a “local-aware formatting of a relative date or time”. E.g. something like I want to have for my notifcations: “Next: In two weeks”. You pass in two dates (one is your reference dates) and it calculates the difference and produces a nice string for you.

Although you can technically concatenate your formatter’s results to an existing string, you have to make sure that this will not conflict in a grammatical sense.

The code documentation also says that you should use the formatter’s results only “in a standalone manner”.

Using the formatter

There are four available methods that allow you to format time in different representations to a string:

func localizedString(for: Date, relativeTo: Date) -> String
func localizedString(from: DateComponents) -> String
func localizedString(fromTimeInterval: TimeInterval) -> String
func string(for: Any?) -> String?Code language: JavaScript (javascript)

The one you would use most commonly, might be localizedString(for: Date, relativeTo: Date). Imagine, someone is standing at a certain point in time (your second parameter) reasoning about another date (which is your first parameter). That means, your second parameter is in most cases Date().

What is really cool about the formatter is that it not only handles plurals for you, it also aproximates really nice. So, it does not just format a date that is ten days in the past to “10 days ago”, it will actually format everything that makes sense to “last week”, “next week”, etc.

Let’s see an easy example of the formatter in action:

let formatter = RelativeDateTimeFormatter()
let calendar = Calendar.current
let today = Date()

// Some other points in time. 
if let lastWeek = calendar.date(byAdding: DateComponents(day: -10),
        to: Date()),
   let lastWeekAsWell = calendar.date(byAdding: DateComponents(day: -8),
        to: Date()),
   let twoWeeksAgo = calendar.date(byAdding: DateComponents(day: -18),
           to: Date()),
   let lastMonth = calendar.date(byAdding: DateComponents(day: -40),
         to: Date()) {
   let nextYear = calendar.date(byAdding: DateComponents(month: 16),
        to: today)
 formatter.localizedString(for: lastWeek, relativeTo: today)
 formatter.localizedString(for: lastWeekAsWell, relativeTo: today)
 formatter.localizedString(for: twoWeeksAgo, relativeTo: today)
 formatter.localizedString(for: lastMonth, relativeTo: today)
 formatter.localizedString(for: nextYear, relativeTo: today)

 // Prints:
 // "1 week ago"
 // "1 week ago"
 // "2 weeks ago"
 // "1 month ago"
 // "in 1 year"
}Code language: Swift (swift)

As you can see above, the formatter approximates eight days in the past and ten days in the past to “1 week ago” which is pretty cool, right?

Shorthand method for the RelativeDateTimeFormatter

There is another method that is inherited from Formatter which accepts Any (func string(for: Any?) -> String?) However, it will only return something else than nil if you pass in a Date. It uses Date() as reference date, therefore you can use it as a shorthand for the first presented method. It will give you the same result:

formatter.string(for: lastWeek)
formatter.string(for: lastWeekAsWell)
formatter.string(for: twoWeeksAgo)
formatter.string(for: lastMonth)
formatter.string(for: nextYear)

// Prints the same as above, given the properties above hold the same values
// "1 week ago"
// "1 week ago"
// "2 weeks ago"
// "1 month ago"
// "in 1 year"Code language: JavaScript (javascript)

What if you want the exact amount of days, weeks, years or whatever? You can use func localizedString(from: DateComponents) for that. It will use Date() as a default reference and the formatter does not approximate anything (like we saw before). If you use the same quantities we added above to today you will get the exact amounts in your string:

formatter.localizedString(from: DateComponents(day: -10))
formatter.localizedString(from: DateComponents(day: -8))
formatter.localizedString(from: DateComponents(day: -18))
formatter.localizedString(from: DateComponents(day: -40))
formatter.localizedString(from: DateComponents(month: 16))

// Prints:
// "10 days ago"
// "8 days ago"
// "18 days ago"
// "40 days ago"
// "in 16 months"Code language: JavaScript (javascript)

The third method localizedString(fromTimeInterval: TimeInterval) is a convenience method for creating a string from a TimeInterval. It behaves like the method for creating a string from DateComponents. However, for longer timespans I recommend not to use it. DateComponents(day: 5) reads way better than 60 * 60 * 24 * 5. In addition to that, you will loose accuracy when using TimeInterval for longer periods.

The fourth method is inherited from Formatter that is why you can pass in Any. However, it will only return something else than nil if you pass in a Date. It uses Date() as reference date, therefore you can use it as a shorthand for the first presented method:

Now that we learnt something about the different methods, let’s see how we can configure the formatter.

Configuring the RelativeDateTimeFormatter

You should have now an understanding about how to use the RelativeDateTimeFormatter in Swift. So, it does a pretty great job without configuring anything. Actually, this formatter does not offer a lot of configuring properties but I think it is sufficient in most cases.

In the following listing you can see all the available properties with some documentation by me. You should be familar with some of them if you already worked with DateFormatter:

/// You can set this property to .named or .numeric.
/// .numeric will give you "1 day ago", "1 week ago", etc.
/// .named will give you "yesterday", "last week", etc.
var dateTimeStyle: RelativeDateTimeFormatter.DateTimeStyle


/// Use this to modify how the units and quantaties are formatted.
/// E.g. for a date that is 2 days in the past:
/// .full will result in "2 days ago"
/// .abbreviated will result in "2 d. ago"
var unitsStyle: RelativeDateTimeFormatter.UnitsStyle

/// This sets where the formatted string will be used to format correctly.
/// E.g. in a lot of languages the first letter of a sentence is capatalized.
/// You can just use .beginningOfSentence  for that.
/// In other cases you might use .
var formattingContext: Formatter.Context
Code language: Swift (swift)

There are two other ones you probably will not need to change because you should respect whatever the users have set on their devices:

/// The calendar used by the formatter.
/// E.g. .chinese, .gregorian, .hebrew and a lot more.
/// Normally you do not have to set this.
/// You should let the system apply whatever users picked on their phone.
/// Defaults to Calendar.autoupdatingCalendar.
var calendar: Calendar!

/// Similar to calendar, normally you do not need to set this.
/// By default, the calendar's locale is used here.
var locale: Locale!Code language: PHP (php)

More complex time formatting

This post should have given you a short introduction into how to use the RelativeDateTimeFormatter in Swift. However, there are cases where this formatter might not be enough. If you want to format dates that should be placed in a text or need more complex formatting behavior in general, I would rather use DateComponentsFormatter together with NSLocalizedString. If you use RelativeDateTimeFormatter in non-standalone places you might end up with funny looking texts. You should be aware that it might look totally fine in your preferred language but it might not work in other languages.

Happy time formatting! ⏰

Leave a Reply

Your email address will not be published. Required fields are marked *