RelativeDateTimeFormatter — это класс Foundation в iOS и macOS, который преобразует абсолютные даты в человекочитаемые относительные формулировки: «5 минут назад», «вчера», «через 3 дня». По данным Apple Developer Documentation, 2024, RelativeDateTimeFormatter автоматически выбирает подходящий юнит (секунды, минуты, часы, дни) и локализует вывод на языке текущей локали устройства. В отличие от ручного расчёта разницы между датами через Calendar, этот класс учитывает лингвистические особенности каждого языка: для одних языков склоняются числительные, для других — используется особая форма для слова «вчера». Класс доступен начиная с iOS 13 и macOS 10.15.
Главное
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 состоит из трёх шагов: вычисление разницы между переданной датой и текущим моментом, выбор подходящего юнита (самого большого, который не даёт ноль) и форматирование с учётом локали. Например, для разницы в 3720 секунд (1 час 2 минуты) будет выбран юнит «час», и результат будет «1 час назад», а не «62 минуты назад».
Юниты выбираются по принципу «наибольшего не нулевого»: если разница больше 86400 секунд (1 день), используются дни; если больше 604800 (1 неделя) — недели, и так далее. Этот алгоритм гарантирует, что результат всегда читается естественно: вместо «518400 секунд назад» пользователь видит «6 дней назад». Точные границы юнитов определяются календарём текущей локали.
| Диапазон разницы | Юнит | Пример для ru_RU |
|---|---|---|
| 0–59 секунд | Seconds | 30 секунд назад |
| 1–59 минут | Minutes | 5 минут назад |
| 1–23 часа | Hours | 3 часа назад |
| 1–6 дней | Days | 2 дня назад |
| 7–27 дней | Weeks | 1 неделю назад |
| 28 дней–11 месяцев | Months | 3 месяца назад |
| 12+ месяцев | Years | 1 год назад |
Контекст форматирования определяет окончание фразы. Для прошлого: «назад» (русский), «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 минут назад».
Ограничение юнитов: по умолчанию RelativeDateTimeFormatter отображает только один (самый большой) юнит. Установка maximumUnitCount = 2 включает следующий юнит для более точного описания: «1 час 30 минут назад». Однако это может сделать строку избыточно длинной для коротких сообщений (пуши, уведомления). Для UI рекомендуется оставить 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()))")
Выбор стиля для разных контекстов: для ленты новостей используйте .numeric с maximumUnitCount = 1 — это стандарт для Twitter, Instagram и Facebook. Для Accessibility (VoiceOver) используйте .full — прописные числа читаются естественнее. Для компактных элементов (уголок уведомления, строка статуса) используйте .abbreviated для экономии места.
Базовое использование RelativeDateTimeFormatter сводится к созданию экземпляра, настройке свойств и вызову одного из методов форматирования. Основные методы: localizedString(for:relativeTo:) — для пары дат, localizedString(fromTimeInterval:) — для разницы в секундах, и string(for:) — для Date с автоматическим контекстом (прошлое/будущее).
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 секунд) — отображать «только что» вручную, иначе передавать дату в форматтер.
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-правила, без необходимости дополнительного кода.
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 — класс Foundation для отображения дат в относительном формате: «5 минут назад», «через 2 дня». Доступен с iOS 13 и macOS 10.15.
По принципу наибольшего не нулевого юнита — секунды, минуты, часы, дни, недели, месяцы или годы. Например, для разницы 3720 секунд (1 час 2 минуты) будет выбран юнит «час», а не «минуты».
Установите свойство locale на нужный экземпляр Locale. По умолчанию используется Locale.current. Пример: formatter.locale = Locale(identifier: "de_DE") для немецкого языка.
.numeric — полная форма («3 дня назад»), .abbreviated — сокращённая («3 дн. назад»). Выбор зависит от контекста: numeric для основного UI, abbreviated для компактных элементов.
Добавьте ручную проверку на интервал менее 5–10 секунд. RelativeDateTimeFormatter не поддерживает «только что» — для малых интервалов он возвращает «0 секунд назад». Используйте условную логику с порогом.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также