RelativeDateTimeFormatter: суть, относительные даты и Swift

Автор: IT Sectr Опубликовано: 2026-07-13 Время чтения: 10 мин

RelativeDateTimeFormatter — это класс Foundation в iOS и macOS, который преобразует абсолютные даты в человекочитаемые относительные формулировки: «5 минут назад», «вчера», «через 3 дня». По данным Apple Developer Documentation, 2024, RelativeDateTimeFormatter автоматически выбирает подходящий юнит (секунды, минуты, часы, дни) и локализует вывод на языке текущей локали устройства. В отличие от ручного расчёта разницы между датами через Calendar, этот класс учитывает лингвистические особенности каждого языка: для одних языков склоняются числительные, для других — используется особая форма для слова «вчера». Класс доступен начиная с iOS 13 и macOS 10.15.

Главное

  • RelativeDateTimeFormatter — класс для отображения относительных дат в iOS и macOS (iOS 13+)
  • Локализованный вывод — автоматически подбирает формулировки на языке текущей локали
  • Три типа контекста — past (назад), future (через), present (сейчас) с разными формулировками
  • Автоматический выбор юнита — секунды, минуты, часы, дни, недели, месяцы, годы
  • Настройка стиля — numeric (через 3 дня) или abbreviated (через 3 дн.)

Что такое RelativeDateTimeFormatter?

RelativeDateTimeFormatter — это подкласс Formatter в Foundation, который принимает Date (или разницу в секундах) и возвращает локализованную строку с относительным временем. Например, для даты на 5 минут раньше текущей он вернёт «5 минут назад» для ru_RU или «5 minutes ago» для en_US. Класс поддерживает три временных контекста: прошлое (past), будущее (future) и настоящее (present).

Внутренняя логика RelativeDateTimeFormatter использует Calendar и Locale для расчёта разницы между датами и выбора правильной грамматической формы. Для русского языка класс корректно склоняет числительные: «1 минуту назад», «2 минуты назад», «5 минут назад». Для английского — выбирает между «minute ago» и «minutes ago». Эта функциональность основана на данных ICU (International Components for Unicode) и не требует дополнительной настройки от разработчика.

По данным Apple WWDC 2019, RelativeDateTimeFormatter стал частью фреймворка для упрощения локализации — до его появления разработчики были вынуждены вручную рассчитывать разницу дат и подставлять локализованные строки через String.localizedStringWithFormat. Это приводило к ошибкам в склонении (особенно для славянских и арабских языков) и некорректному выбору единиц измерения.

Как RelativeDateTimeFormatter отображает «5 минут назад»?

Алгоритм работы RelativeDateTimeFormatter состоит из трёх шагов: вычисление разницы между переданной датой и текущим моментом, выбор подходящего юнита (самого большого, который не даёт ноль) и форматирование с учётом локали. Например, для разницы в 3720 секунд (1 час 2 минуты) будет выбран юнит «час», и результат будет «1 час назад», а не «62 минуты назад».

Юниты выбираются по принципу «наибольшего не нулевого»: если разница больше 86400 секунд (1 день), используются дни; если больше 604800 (1 неделя) — недели, и так далее. Этот алгоритм гарантирует, что результат всегда читается естественно: вместо «518400 секунд назад» пользователь видит «6 дней назад». Точные границы юнитов определяются календарём текущей локали.

Диапазон разницыЮнитПример для ru_RU
0–59 секундSeconds30 секунд назад
1–59 минутMinutes5 минут назад
1–23 часаHours3 часа назад
1–6 днейDays2 дня назад
7–27 днейWeeks1 неделю назад
28 дней–11 месяцевMonths3 месяца назад
12+ месяцевYears1 год назад

Контекст форматирования определяет окончание фразы. Для прошлого: «назад» (русский), «ago» (английский). Для будущего: «через 3 дня» (русский), «in 3 days» (английский). Для настоящего: «сейчас» (русский), «now» (английский). Контекст задаётся через метод localizeString(fromTimeInterval:) или напрямую через string(from: Date).

Настройки юнитов и стилей

RelativeDateTimeFormatter предоставляет несколько настроек для контроля вывода: свойство unitsStyle определяет стиль форматирования (numeric, abbreviated, full, spellOut), а maximumUnitCount ограничивает количество отображаемых юнитов. Например, с maximumUnitCount = 1 разница в 1 час 30 минут будет показана как «1 час назад» вместо «1 час 30 минут назад».

Стили форматирования

  • .numeric — полное числовое значение: «3 дня назад», «через 2 недели». Рекомендуется для UI по умолчанию
  • .abbreviated — сокращённая форма: «3 дн. назад», «через 2 нед.». Для компактного отображения в таблицах и списках
  • .full — словесная форма без цифр: «три дня назад». Для Accessibility и голосовых интерфейсов
  • .spellOut — буквенная форма с альтернативным написанием: «three days ago». Используется редко, в основном для specialised use

Ограничение юнитов: по умолчанию RelativeDateTimeFormatter отображает только один (самый большой) юнит. Установка maximumUnitCount = 2 включает следующий юнит для более точного описания: «1 час 30 минут назад». Однако это может сделать строку избыточно длинной для коротких сообщений (пуши, уведомления). Для UI рекомендуется оставить 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()))")

Выбор стиля для разных контекстов: для ленты новостей используйте .numeric с maximumUnitCount = 1 — это стандарт для Twitter, Instagram и Facebook. Для Accessibility (VoiceOver) используйте .full — прописные числа читаются естественнее. Для компактных элементов (уголок уведомления, строка статуса) используйте .abbreviated для экономии места.

RelativeDateTimeFormatter в Swift: примеры

Базовое использование RelativeDateTimeFormatter сводится к созданию экземпляра, настройке свойств и вызову одного из методов форматирования. Основные методы: localizedString(for:relativeTo:) — для пары дат, localizedString(fromTimeInterval:) — для разницы в секундах, и string(for:) — для Date с автоматическим контекстом (прошлое/будущее).

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

Обработка сценария «только что» — RelativeDateTimeFormatter не имеет встроенной поддержки фразы «только что» для очень малых интервалов. Для разницы менее 5 секунд он вернёт «0 секунд назад», что некрасиво в UI. Рекомендуется оборачивать вызов formatter в условную логику: если разница меньше установленного порога (например, 5 секунд) — отображать «только что» вручную, иначе передавать дату в форматтер.

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

Метод string(fromTimeInterval:) принимает разницу в секундах и автоматически определяет контекст (положительное значение — будущее, отрицательное — прошлое). Это удобно, когда разница уже известна (например, получена от сервера в unix timestamp). Для этого случая не нужно создавать Date — разница передаётся напрямую.

Локализация относительных дат

RelativeDateTimeFormatter автоматически локализует вывод на основе Locale.current. Для смены языка форматирования установите свойство locale — в отличие от DateFormatter, для RelativeDateTimeFormatter locale не фиксируется, и его можно менять для каждого вызова. Это позволяет отображать относительные даты на языке, отличном от языка интерфейса (например, контент на языке оригинала).

Сложность локализации относительных дат заключается в грамматических особенностях разных языков. Русский язык требует различных форм числительных: «1 минута», «2 минуты», «5 минут». Арабский — использует форму множественного числа для чисел от 3 до 10 и особые формы для 11+. Китайский — не имеет склонения вообще, что упрощает задачу. RelativeDateTimeFormatter покрывает все эти случаи через ICU-правила, без необходимости дополнительного кода.

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 Russian 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()))")
}

Важный нюанс: RelativeDateTimeFormatter игнорирует TimeZone при расчёте разницы для настроек .numeric — он использует абсолютную разницу в секундах. Однако для .full стиля (с прописными числами) и особых случаев (вчера, сегодня) TimeZone учитывается. Всегда устанавливайте TimeZone явно для консистентности, особенно если приложение работает с серверными датами в UTC.

Типовые ошибки при форматировании

Игнорирование TimeZone при расчёте относительных дат — частая ошибка при работе с серверными датами. Если сервер присылает Date в UTC, а RelativeDateTimeFormatter использует TimeZone.current, разница может быть вычислена некорректно для дат, близких к текущему моменту. Рекомендуется всегда устанавливать formatter.timeZone = TimeZone(secondsFromGMT: 0) для серверных данных.

Неправильный выбор юнита для коротких интервалов — RelativeDateTimeFormatter округляет разницу до наибольшего юнита. Для 25 часов результат будет «1 день назад», что может ввести пользователя в заблуждение. Если необходима высокая точность (например, для таймеров обратного отсчёта), используйте DateComponentsFormatter вместо RelativeDateTimeFormatter — он позволяет отображать несколько юнитов одновременно.

Отсутствие проверки на negative TimeInterval — если дата в будущем передаётся как прошлая (отрицательное значение в string(fromTimeInterval:)), форматтер может вернуть некорректную строку. Всегда проверяйте знак интервала перед передачей в форматтер, особенно при работе с серверными данными, где часовой пояс может исказить расчёт.

По данным Hacker News (2024), одна из наиболее обсуждаемых проблем RelativeDateTimeFormatter — отсутствие встроенной поддержки «вчера» и «сегодня» для английского языка. Вместо «вчера» форматтер для разницы 90000 секунд вернёт «1 день назад». Для русского языка такой проблемы нет — «1 день назад» звучит естественно, но для английского UI «yesterday» предпочтительнее. Эта функциональность не поддерживается и требует ручной проверки через Calendar.isDateInToday/Yesterday.

Часто задаваемые вопросы

Что такое RelativeDateTimeFormatter?

RelativeDateTimeFormatter — класс Foundation для отображения дат в относительном формате: «5 минут назад», «через 2 дня». Доступен с iOS 13 и macOS 10.15.

Как RelativeDateTimeFormatter выбирает юниты?

По принципу наибольшего не нулевого юнита — секунды, минуты, часы, дни, недели, месяцы или годы. Например, для разницы 3720 секунд (1 час 2 минуты) будет выбран юнит «час», а не «минуты».

Как изменить язык вывода?

Установите свойство locale на нужный экземпляр Locale. По умолчанию используется Locale.current. Пример: formatter.locale = Locale(identifier: "de_DE") для немецкого языка.

В чем разница между .numeric и .abbreviated?

.numeric — полная форма («3 дня назад»), .abbreviated — сокращённая («3 дн. назад»). Выбор зависит от контекста: numeric для основного UI, abbreviated для компактных элементов.

Как отобразить «только что» вместо «0 секунд назад»?

Добавьте ручную проверку на интервал менее 5–10 секунд. RelativeDateTimeFormatter не поддерживает «только что» — для малых интервалов он возвращает «0 секунд назад». Используйте условную логику с порогом.

Итоги

  • RelativeDateTimeFormatter — удобный класс для отображения относительных дат в iOS 13+
  • Автоматическая локализация — корректное склонение для всех поддерживаемых языков через ICU
  • Три стиля — .numeric (стандарт), .abbreviated (компактный), .full (прописью)
  • Выбор юнита — автоматический по принципу наибольшего не нулевого значения
  • Настройка TimeZone — обязательна для консистентности при работе с серверными датами
  • Порог «только что» — не поддерживается встроенным; требуется ручная проверка интервала
  • Нет поддержки «вчера» — для английского языка форматтер не использует форму yesterday

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также