RelativeDateTimeFormatter is a Foundation class in iOS and macOS that converts absolute dates into human-readable relative phrases: "5 minutes ago", "yesterday", "in 3 days". According to Apple Developer Documentation, 2024, RelativeDateTimeFormatter automatically selects the appropriate unit (seconds, minutes, hours, days) and localizes the output in the language of the device's current locale. Unlike manually calculating the difference between dates through Calendar, this class takes into account the linguistic features of each language: for some languages numerals are inflected, for others a special form for the word "yesterday" is used. The class is available starting from iOS 13 and macOS 10.15.
Key Takeaways
RelativeDateTimeFormatter is a subclass of Formatter in Foundation that takes a Date (or a difference in seconds) and returns a localized string with relative time. For example, for a date 5 minutes before the current one, it returns "5 minutes ago" for en_US. The class supports three temporal contexts: past, future, and present.
The internal logic of RelativeDateTimeFormatter uses Calendar and Locale to calculate the difference between dates and select the correct grammatical form. For English, it chooses between "minute ago" and "minutes ago". This functionality is based on ICU (International Components for Unicode) data and does not require additional configuration from the developer.
According to Apple WWDC 2019, RelativeDateTimeFormatter became part of the framework to simplify localization — before its introduction, developers had to manually calculate date differences and substitute localized strings via String.localizedStringWithFormat. This led to inflection errors (especially for Slavic and Arabic languages) and incorrect selection of measurement units.
The algorithm of RelativeDateTimeFormatter consists of three steps: calculating the difference between the passed date and the current moment, selecting the appropriate unit (the largest one that does not yield zero), and formatting according to the locale. For example, for a difference of 3720 seconds (1 hour 2 minutes), the unit "hour" is selected, and the result is "1 hour ago", not "62 minutes ago".
Units are selected on the principle of "largest non-zero": if the difference is greater than 86400 seconds (1 day), days are used; if greater than 604800 (1 week) — weeks, and so on. This algorithm ensures that the result always reads naturally: instead of "518400 seconds ago", the user sees "6 days ago". The exact boundaries of units are determined by the calendar of the current locale.
| Difference range | Unit | Example for en_US |
|---|---|---|
| 0–59 seconds | Seconds | 30 seconds ago |
| 1–59 minutes | Minutes | 5 minutes ago |
| 1–23 hours | Hours | 3 hours ago |
| 1–6 days | Days | 2 days ago |
| 7–27 days | Weeks | 1 week ago |
| 28 days–11 months | Months | 3 months ago |
| 12+ months | Years | 1 year ago |
The formatting context determines the ending of the phrase. For the past: "ago" (English). For the future: "in 3 days" (English). For the present: "now" (English). The context is set via the localizeString(fromTimeInterval:) method or directly through string(from: Date).
RelativeDateTimeFormatter provides several settings to control the output: the unitsStyle property determines the formatting style (numeric, abbreviated, full, spellOut), and maximumUnitCount limits the number of displayed units. For example, with maximumUnitCount = 1, a difference of 1 hour 30 minutes is shown as "1 hour ago" instead of "1 hour 30 minutes ago".
Unit limiting: by default, RelativeDateTimeFormatter displays only one (the largest) unit. Setting maximumUnitCount = 2 includes the next unit for a more precise description: "1 hour 30 minutes ago". However, this can make the string excessively long for short messages (push notifications, alerts). For UI, it is recommended to keep maximumUnitCount = 1.
import Foundation
let formatter = RelativeDateTimeFormatter()
// Configure styles
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Examples with different dates
let fiveMinAgo = Date().addingTimeInterval(-300)
print("5 min ago: \(formatter.localizedString(for: fiveMinAgo, relativeTo: Date()))")
let twoDaysLater = Date().addingTimeInterval(172800)
print("2 days later: \(formatter.localizedString(for: twoDaysLater, relativeTo: Date()))")
// Abbreviated style
formatter.unitsStyle = .abbreviated
let oneWeekAgo = Date().addingTimeInterval(-604800)
print("Abbreviated: \(formatter.localizedString(for: oneWeekAgo, relativeTo: Date()))")
// Full style (spelled out)
formatter.unitsStyle = .full
let threeHours = Date().addingTimeInterval(10800)
print("Full: \(formatter.localizedString(for: threeHours, relativeTo: Date()))")
Choosing a style for different contexts: for a news feed, use .numeric with maximumUnitCount = 1 — this is the standard for Twitter, Instagram, and Facebook. For Accessibility (VoiceOver), use .full — spelled-out numbers are read more naturally. For compact elements (notification badge, status bar), use .abbreviated to save space.
Basic usage of RelativeDateTimeFormatter boils down to creating an instance, configuring properties, and calling one of the formatting methods. The main methods are: localizedString(for:relativeTo:) — for a pair of dates, localizedString(fromTimeInterval:) — for a difference in seconds, and string(for:) — for Date with automatic context (past/future).
import Foundation
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Social network UI examples
let postDates: [(title: String, date: Date)] = [
("Just now", Date().addingTimeInterval(-30)),
("5 min ago", Date().addingTimeInterval(-300)),
("Yesterday", Date().addingTimeInterval(-90000)),
("Last week", Date().addingTimeInterval(-700000)),
("Last year", Date().addingTimeInterval(-32000000))
]
for (title, postDate) in postDates {
let relative = formatter.localizedString(
for: postDate,
relativeTo: Date()
)
print("\(title): \(relative)")
}
// Future dates
let reminderFormatter = RelativeDateTimeFormatter()
reminderFormatter.unitsStyle = .abbreviated
let inOneHour = Date().addingTimeInterval(3600)
let reminderText = reminderFormatter.localizedString(
for: inOneHour,
relativeTo: Date()
)
print("Reminder: \(reminderText)")
Handling the "just now" scenario — RelativeDateTimeFormatter has no built-in support for the phrase "just now" for very small intervals. For a difference of less than 5 seconds, it returns "0 seconds ago", which looks bad in UI. It is recommended to wrap the formatter call in conditional logic: if the difference is less than a set threshold (e.g., 5 seconds) — display "just now" manually, otherwise pass the date to the formatter.
import Foundation
func relativeTimeString(from date: Date) -> String {
let interval = Date().timeIntervalSince(date)
// "Just now" threshold
if interval < 5 {
return "just now"
}
// "Today" threshold
if interval < 60 {
return "just now"
}
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Display without "ago" suffix
return formatter.localizedString(
for: date,
relativeTo: Date()
)
}
print(relativeTimeString(from: Date().addingTimeInterval(-3)))
print(relativeTimeString(from: Date().addingTimeInterval(-120)))
print(relativeTimeString(from: Date().addingTimeInterval(-3600)))
The string(fromTimeInterval:) method accepts a difference in seconds and automatically determines the context (positive value — future, negative — past). This is convenient when the difference is already known (e.g., received from the server as a unix timestamp). In this case, there is no need to create a Date — the difference is passed directly.
RelativeDateTimeFormatter automatically localizes the output based on Locale.current. To change the formatting language, set the locale property — unlike DateFormatter, for RelativeDateTimeFormatter the locale is not fixed and can be changed for each call. This allows displaying relative dates in a language different from the interface language (e.g., content in the original language).
The complexity of localizing relative dates lies in the grammatical features of different languages. English requires different numeral forms: "1 minute", "2 minutes". Arabic uses the plural form for numbers from 3 to 10 and special forms for 11+. Chinese has no inflection at all, which simplifies the task. RelativeDateTimeFormatter covers all these cases through ICU rules, without the need for additional code.
import Foundation
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
let targetDate = Date().addingTimeInterval(-7200) // 2 hours ago
// Different locales
let locales: [String] = ["ru_RU", "en_US", "de_DE", "fr_FR", "ja_JP", "ar_SA"]
for identifier in locales {
formatter.locale = Locale(identifier: identifier)
let result = formatter.localizedString(
for: targetDate,
relativeTo: Date()
)
print("\(identifier): \(result)")
}
// Check English pluralization
formatter.locale = Locale(identifier: "ru_RU")
let intervals: [TimeInterval] = [-60, -120, -180, -300]
for interval in intervals {
let date = Date().addingTimeInterval(interval)
print("\(-Int(interval / 60)) min: \(formatter.localizedString(for: date, relativeTo: Date()))")
}
An important nuance: RelativeDateTimeFormatter ignores TimeZone when calculating the difference for .numeric settings — it uses the absolute difference in seconds. However, for the .full style (with spelled-out numbers) and special cases (yesterday, today), TimeZone is taken into account. Always set TimeZone explicitly for consistency, especially if the application works with server dates in UTC.
Ignoring TimeZone when calculating relative dates — a common mistake when working with server dates. If the server sends a Date in UTC, and RelativeDateTimeFormatter uses TimeZone.current, the difference may be calculated incorrectly for dates close to the current moment. It is recommended to always set formatter.timeZone = TimeZone(secondsFromGMT: 0) for server data.
Incorrect unit selection for short intervals — RelativeDateTimeFormatter rounds the difference to the largest unit. For 25 hours, the result will be "1 day ago", which may mislead the user. If high precision is needed (e.g., for countdown timers), use DateComponentsFormatter instead of RelativeDateTimeFormatter — it allows displaying multiple units simultaneously.
No check for negative TimeInterval — if a future date is passed as past (negative value in string(fromTimeInterval:)), the formatter may return an incorrect string. Always check the sign of the interval before passing it to the formatter, especially when working with server data where the time zone may distort the calculation.
According to Hacker News (2024), one of the most discussed issues of RelativeDateTimeFormatter is the lack of built-in support for "yesterday" and "today" for the English language. Instead of "yesterday", the formatter for a difference of 90000 seconds returns "1 day ago". For the Russian language, there is no such problem — "1 day ago" sounds natural, but for English UI "yesterday" is preferable. This functionality is not supported and requires manual checking via Calendar.isDateInToday/Yesterday.
Frequently Asked Questions
RelativeDateTimeFormatter is a Foundation class for displaying dates in a relative format: "5 minutes ago", "in 2 days". Available since iOS 13 and macOS 10.15.
By the principle of the largest non-zero unit — seconds, minutes, hours, days, weeks, months, or years. For example, for a difference of 3720 seconds (1 hour 2 minutes), the unit "hour" is selected, not "minutes".
Set the locale property to the desired Locale instance. By default, Locale.current is used. Example: formatter.locale = Locale(identifier: "de_DE") for German.
.numeric — full form ("3 days ago"), .abbreviated — shortened form ("3 d. ago"). The choice depends on the context: numeric for main UI, abbreviated for compact elements.
Add a manual check for an interval of less than 5-10 seconds. RelativeDateTimeFormatter does not support "just now" — for small intervals it returns "0 seconds ago". Use conditional logic with a threshold.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also