RelativeDateTimeFormatter: essence, relative dates and Swift

Author: IT Sectr Published: 2026-07-13 Reading time: 10 min

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 — a class for displaying relative dates in iOS and macOS (iOS 13+)
  • Localized output — automatically picks phrasing in the language of the current locale
  • Three context types — past (ago), future (in), present (now) with different phrasings
  • Automatic unit selection — seconds, minutes, hours, days, weeks, months, years
  • Style customization — numeric (in 3 days) or abbreviated (in 3 d.)

What is RelativeDateTimeFormatter?

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.

How does RelativeDateTimeFormatter display "5 minutes ago"?

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 rangeUnitExample for en_US
0–59 secondsSeconds30 seconds ago
1–59 minutesMinutes5 minutes ago
1–23 hoursHours3 hours ago
1–6 daysDays2 days ago
7–27 daysWeeks1 week ago
28 days–11 monthsMonths3 months ago
12+ monthsYears1 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).

Unit and style settings

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".

Formatting styles

  • .numeric — full numeric value: "3 days ago", "in 2 weeks". Recommended for UI by default
  • .abbreviated — shortened form: "3 d. ago", "in 2 wk.". For compact display in tables and lists
  • .full — verbal form without digits: "three days ago". For Accessibility and voice interfaces
  • .spellOut — letter form with alternative spelling: "three days ago". Rarely used, mainly for specialized use

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.

swift
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.

RelativeDateTimeFormatter in Swift: examples

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).

swift
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.

swift
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.

Localization of relative dates

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.

swift
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.

Common formatting errors

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

What is RelativeDateTimeFormatter?

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.

How does RelativeDateTimeFormatter select units?

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".

How to change the output language?

Set the locale property to the desired Locale instance. By default, Locale.current is used. Example: formatter.locale = Locale(identifier: "de_DE") for German.

What is the difference between .numeric and .abbreviated?

.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.

How to display "just now" instead of "0 seconds ago"?

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

  • RelativeDateTimeFormatter — a convenient class for displaying relative dates in iOS 13+
  • Automatic localization — correct inflection for all supported languages via ICU
  • Three styles — .numeric (standard), .abbreviated (compact), .full (spelled out)
  • Unit selection — automatic based on the largest non-zero value
  • TimeZone configuration — mandatory for consistency when working with server dates
  • "Just now" threshold — not supported built-in; requires manual interval check
  • No "yesterday" support — the formatter does not use the yesterday form for English

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.

Discuss the project

Read also