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 хвилин тому». Клас підтримує три часових контексти: минуле, майбутнє та теперішнє.

Внутрішня логіка RelativeDateTimeFormatter використовує Calendar та Locale для розрахунку різниці між датами та вибору правильної граматичної форми. Для української мови клас коректно відмінює числівники: «1 хвилину тому», «2 хвилини тому», «5 хвилин тому». Ця функціональність базується на даних 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 днів тому». Точні межі юнітів визначаються календарем поточної локалі.

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

Контекст форматування визначає закінчення фрази. Для минулого: «тому» (українська). Для майбутнього: «через 3 дні» (українська). Для теперішнього: «зараз» (українська). Контекст задається через метод 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()

// 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 Ukrainian 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

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