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 секунди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 минути“.

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

  • .numeric — пълна числова стойност: „преди 3 дни“, „след 2 седмици“. Препоръчва се за UI по подразбиране
  • .abbreviated — съкратена форма: „преди 3 дни“, „след 2 седм.“. За компактно показване в таблици и списъци
  • .full — словесна форма без цифри: „преди три дни“. За Accessibility и гласови интерфейси
  • .spellOut — буквена форма с алтернативен правопис: „three days ago“. Използва се рядко, главно за специални цели

Ограничение на единиците: по подразбиране RelativeDateTimeFormatter показва само една (най-голямата) единица. Задаването на maximumUnitCount = 2 включва следващата единица за по-точно описание: „преди 1 час 30 минути“. Това обаче може да направи низа прекалено дълъг за кратки съобщения (пуш, известия). За UI се препоръчва да се запази maximumUnitCount = 1.

swift
import Foundation

let formatter = RelativeDateTimeFormatter()

// Конфигурирайте стиловете
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1

// Примери с различни дати
let fiveMinAgo = Date().addingTimeInterval(-300)
print("преди 5 мин: \(formatter.localizedString(for: fiveMinAgo, relativeTo: Date()))")

let twoDaysLater = Date().addingTimeInterval(172800)
print("след 2 дни: \(formatter.localizedString(for: twoDaysLater, relativeTo: Date()))")

// Съкратен стил
formatter.unitsStyle = .abbreviated
let oneWeekAgo = Date().addingTimeInterval(-604800)
print("Съкратен: \(formatter.localizedString(for: oneWeekAgo, relativeTo: Date()))")

// Пълен стил (с думи)
formatter.unitsStyle = .full
let threeHours = Date().addingTimeInterval(10800)
print("Пълен: \(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

// Примери за UI на социална мрежа
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)")
}

// Бъдещи дати
let reminderFormatter = RelativeDateTimeFormatter()
reminderFormatter.unitsStyle = .abbreviated
let inOneHour = Date().addingTimeInterval(3600)
let reminderText = reminderFormatter.localizedString(
    for: inOneHour,
    relativeTo: Date()
)
print("Напомняне: \(reminderText)")

Обработка на сценария „току-що“ — RelativeDateTimeFormatter няма вградена поддръжка за фразата „току-що“ за много малки интервали. За разлика по-малка от 5 секунди ще върне „преди 0 секунди“, което изглежда грозно в UI. Препоръчва се извикването на formatter да се обвие в условна логика: ако разликата е по-малка от зададения праг (например 5 секунди) — покажете „току-що“ ръчно, в противен случай предайте датата на форматтера.

swift
import Foundation

func relativeTimeString(from date: Date) -> String {
    let interval = Date().timeIntervalSince(date)

    // Праг „току-що“
    if interval < 5 {
        return "just now"
    }

    // Праг „днес“
    if interval < 60 {
        return "just now"
    }

    let formatter = RelativeDateTimeFormatter()
    formatter.unitsStyle = .numeric
    formatter.maximumUnitCount = 1

    // Показване без суфикса „назад“
    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 часа

// Различни локали
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)")
}

// Проверете руското скланяне
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)) мин: \(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 — той позволява показване на няколко единици едновременно.

Липса на проверка за отрицателен 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 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също