RelativeDateTimeFormatter: istota, daty względne i Swift

Autor: IT Sectr Opublikowano: 2026-07-13 Czas czytania: 10 min

RelativeDateTimeFormatter — to klasa Foundation w iOS i macOS, która przekształca absolutne daty na czytelne dla człowieka sformułowania względne: „5 minut temu“, „wczoraj“, „za 3 dni“. Według danych Apple Developer Documentation, 2024, RelativeDateTimeFormatter automatycznie wybiera odpowiednią jednostkę (sekundy, minuty, godziny, dni) i lokalizuje wynik w języku bieżącej lokalizacji urządzenia. W przeciwieństwie do ręcznego obliczania różnicy między datami przez Calendar, ta klasa uwzględnia cechy językowe każdego języka: dla jednych języków odmienia liczebniki, dla innych — używa szczególnej formy dla słowa „wczoraj“. Klasa jest dostępna od iOS 13 i macOS 10.15.

Najważniejsze

  • RelativeDateTimeFormatter — klasa do wyświetlania dat względnych w iOS i macOS (iOS 13+)
  • Zlokalizowany wynik — automatycznie dobiera sformułowania w języku bieżącej lokalizacji
  • Trzy typy kontekstu — past (wstecz), future (za), present (teraz) z różnymi sformułowaniami
  • Automatyczny wybór jednostki — sekundy, minuty, godziny, dni, tygodnie, miesiące, lata
  • Konfiguracja stylu — numeric (za 3 dni) lub abbreviated (za 3 dn.)

Co to jest RelativeDateTimeFormatter?

RelativeDateTimeFormatter — to podklasa Formatter w Foundation, która przyjmuje Date (lub różnicę w sekundach) i zwraca zlokalizowany ciąg znaków z czasem względnym. Na przykład dla daty o 5 minut wcześniejszej niż bieżąca zwróci „5 minut temu“ dla ru_RU lub „5 minutes ago“ dla en_US. Klasa obsługuje trzy konteksty czasowe: przeszłość (past), przyszłość (future) i teraźniejszość (present).

Wewnętrzna logika RelativeDateTimeFormatter używa Calendar i Locale do obliczenia różnicy między datami i wyboru właściwej formy gramatycznej. Dla języka polskiego klasa poprawnie odmienia liczebniki: „1 minutę temu“, „2 minuty temu“, „5 minut temu“. Dla angielskiego — wybiera między „minute ago“ a „minutes ago“. Ta funkcjonalność opiera się na danych ICU (International Components for Unicode) i nie wymaga dodatkowej konfiguracji od programisty.

Według danych Apple WWDC 2019, RelativeDateTimeFormatter stał się częścią frameworka w celu uproszczenia lokalizacji — przed jego pojawieniem się programiści musieli ręcznie obliczać różnicę dat i wstawiać zlokalizowane ciągi przez String.localizedStringWithFormat. Prowadziło to do błędów w odmianie (zwłaszcza dla języków słowiańskich i arabskich) i nieprawidłowego wyboru jednostek miary.

Jak RelativeDateTimeFormatter wyświetla „5 minut temu“?

Algorytm działania RelativeDateTimeFormatter składa się z trzech kroków: obliczenie różnicy między przekazaną datą a bieżącym momentem, wybór odpowiedniej jednostki (największej, która nie daje zera) i formatowanie z uwzględnieniem lokalizacji. Na przykład dla różnicy 3720 sekund (1 godzina 2 minuty) zostanie wybrana jednostka „godzina“, a wynik będzie „1 godzina temu“, a nie „62 minuty temu“.

Jednostki są wybierane według zasady „największej niezerowej“: jeśli różnica jest większa niż 86400 sekund (1 dzień), używane są dni; jeśli większa niż 604800 (1 tydzień) — tygodnie i tak dalej. Algorytm ten gwarantuje, że wynik zawsze brzmi naturalnie: zamiast „518400 sekund temu“ użytkownik widzi „6 dni temu“. Dokładne granice jednostek określa kalendarz bieżącej lokalizacji.

Zakres różnicyJednostkaPrzykład dla ru_RU
0–59 sekundSeconds30 sekund temu
1–59 minutMinutes5 minut temu
1–23 godzinHours3 godziny temu
1–6 dniDays2 dni temu
7–27 dniWeeks1 tydzień temu
28 dni–11 miesięcyMonths3 miesiące temu
12+ miesięcyYears1 rok temu

Kontekst formatowania określa zakończenie frazy. Dla przeszłości: „temu“ (polski), „ago“ (angielski). Dla przyszłości: „za 3 dni“ (polski), „in 3 days“ (angielski). Dla teraźniejszości: „teraz“ (polski), „now“ (angielski). Kontekst jest ustawiany metodą localizeString(fromTimeInterval:) lub bezpośrednio przez string(from: Date).

Ustawienia jednostek i stylów

RelativeDateTimeFormatter oferuje kilka ustawień do kontroli wyniku: właściwość unitsStyle określa styl formatowania (numeric, abbreviated, full, spellOut), a maximumUnitCount ogranicza liczbę wyświetlanych jednostek. Na przykład z maximumUnitCount = 1 różnica 1 godzina 30 minut zostanie pokazana jako „1 godzina temu“ zamiast „1 godzina 30 minut temu“.

Style formatowania

  • .numeric — pełna wartość liczbowa: „3 dni temu“, „za 2 tygodnie“. Zalecany dla UI domyślnie
  • .abbreviated — forma skrócona: „3 dn. temu“, „za 2 tyg.“. Do kompaktowego wyświetlania w tabelach i listach
  • .full — forma słowna bez cyfr: „trzy dni temu“. Dla Accessibility i interfejsów głosowych
  • .spellOut — forma literowa z alternatywnym zapisem: „three days ago“. Używany rzadko, głównie do wyspecjalizowanych zastosowań

Ograniczenie jednostek: domyślnie RelativeDateTimeFormatter wyświetla tylko jedną (największą) jednostkę. Ustawienie maximumUnitCount = 2 włącza kolejną jednostkę dla bardziej precyzyjnego opisu: „1 godzina 30 minut temu“. Może to jednak uczynić ciąg nadmiernie długim dla krótkich komunikatów (push, powiadomienia). Dla UI zaleca się pozostawić maximumUnitCount = 1.

swift
import Foundation

let formatter = RelativeDateTimeFormatter()

// Skonfiguruj style
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1

// Przykłady z różnymi datami
let fiveMinAgo = Date().addingTimeInterval(-300)
print("5 min temu: \(formatter.localizedString(for: fiveMinAgo, relativeTo: Date()))")

let twoDaysLater = Date().addingTimeInterval(172800)
print("2 dni później: \(formatter.localizedString(for: twoDaysLater, relativeTo: Date()))")

// Styl skrócony
formatter.unitsStyle = .abbreviated
let oneWeekAgo = Date().addingTimeInterval(-604800)
print("Skrócony: \(formatter.localizedString(for: oneWeekAgo, relativeTo: Date()))")

// Styl pełny (słownie)
formatter.unitsStyle = .full
let threeHours = Date().addingTimeInterval(10800)
print("Pełny: \(formatter.localizedString(for: threeHours, relativeTo: Date()))")

Wybór stylu dla różnych kontekstów: dla kanału wiadomości używaj .numeric z maximumUnitCount = 1 — to standard dla Twittera, Instagrama i Facebooka. Dla Accessibility (VoiceOver) używaj .full — liczby słowne czytają się naturalniej. Dla kompaktowych elementów (róg powiadomienia, pasek statusu) używaj .abbreviated, aby oszczędzić miejsce.

RelativeDateTimeFormatter w Swift: przykłady

Podstawowe użycie RelativeDateTimeFormatter sprowadza się do utworzenia instancji, skonfigurowania właściwości i wywołania jednej z metod formatowania. Główne metody: localizedString(for:relativeTo:) — dla pary dat, localizedString(fromTimeInterval:) — dla różnicy w sekundach oraz string(for:) — dla Date z automatycznym kontekstem (przeszłość/przyszłość).

swift
import Foundation

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

// Przykłady UI sieci społecznościowych
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)")
}

// Daty w przyszłości
let reminderFormatter = RelativeDateTimeFormatter()
reminderFormatter.unitsStyle = .abbreviated
let inOneHour = Date().addingTimeInterval(3600)
let reminderText = reminderFormatter.localizedString(
    for: inOneHour,
    relativeTo: Date()
)
print("Przypomnienie: \(reminderText)")

Obsługa scenariusza „właśnie teraz“ — RelativeDateTimeFormatter nie ma wbudowanej obsługi frazy „właśnie teraz“ dla bardzo małych interwałów. Dla różnicy mniejszej niż 5 sekund zwróci „0 sekund temu“, co nie wygląda dobrze w UI. Zaleca się zawinąć wywołanie formattera w logikę warunkową: jeśli różnica jest mniejsza od ustalonego progu (np. 5 sekund) — wyświetlaj „właśnie teraz“ ręcznie, w przeciwnym razie przekazuj datę do formattera.

swift
import Foundation

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

    // Próg „właśnie teraz“
    if interval < 5 {
        return "just now"
    }

    // Próg „dzisiaj“
    if interval < 60 {
        return "just now"
    }

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

    // Wyświetlanie bez sufiksu „temu“
    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)))

Metoda string(fromTimeInterval:) przyjmuje różnicę w sekundach i automatycznie określa kontekst (wartość dodatnia — przyszłość, ujemna — przeszłość). Jest to wygodne, gdy różnica jest już znana (np. otrzymana z serwera jako unix timestamp). W tym przypadku nie trzeba tworzyć Date — różnica jest przekazywana bezpośrednio.

Lokalizacja dat względnych

RelativeDateTimeFormatter automatycznie lokalizuje wynik na podstawie Locale.current. Aby zmienić język formatowania, ustaw właściwość locale — w przeciwieństwie do DateFormatter, dla RelativeDateTimeFormatter locale nie jest ustalane raz na zawsze i można je zmieniać dla każdego wywołania. Pozwala to wyświetlać daty względne w języku innym niż język interfejsu (np. treść w języku oryginału).

Złożoność lokalizacji dat względnych polega na cechach gramatycznych różnych języków. Język rosyjski wymaga różnych form liczebników: „1 minuta“, „2 minuty“, „5 minut“. Arabski — używa formy liczby mnogiej dla liczb od 3 do 10 i szczególnych form dla 11+. Chiński — nie ma odmiany w ogóle, co upraszcza zadanie. RelativeDateTimeFormatter pokrywa wszystkie te przypadki przez reguły ICU, bez potrzeby dodatkowego kodu.

swift
import Foundation

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

let targetDate = Date().addingTimeInterval(-7200) // 2 godziny temu

// Różne lokalizacje
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)")
}

// Sprawdź rosyjską odmianę liczebników
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()))")
}

Ważny niuans: RelativeDateTimeFormatter ignoruje TimeZone przy obliczaniu różnicy dla ustawień .numeric — używa bezwzględnej różnicy w sekundach. Jednak dla stylu .full (z liczbami słownymi) i szczególnych przypadków (wczoraj, dzisiaj) TimeZone jest uwzględniany. Zawsze ustawiaj TimeZone jawnie dla spójności, zwłaszcza gdy aplikacja pracuje z datami serwera w UTC.

Typowe błędy podczas formatowania

Ignorowanie TimeZone przy obliczaniu dat względnych — częsty błąd podczas pracy z datami serwera. Jeśli serwer wysyła Date w UTC, a RelativeDateTimeFormatter używa TimeZone.current, różnica może zostać obliczona niepoprawnie dla dat bliskich bieżącemu momentowi. Zaleca się zawsze ustawiać formatter.timeZone = TimeZone(secondsFromGMT: 0) dla danych z serwera.

Nieprawidłowy wybór jednostki dla krótkich interwałów — RelativeDateTimeFormatter zaokrągla różnicę do największej jednostki. Dla 25 godzin wynik będzie „1 dzień temu“, co może wprowadzić użytkownika w błąd. Jeśli wymagana jest wysoka precyzja (np. dla timerów odliczania), używaj DateComponentsFormatter zamiast RelativeDateTimeFormatter — pozwala on wyświetlać kilka jednostek jednocześnie.

Brak sprawdzenia ujemnego TimeInterval — jeśli data w przyszłości jest przekazywana jako przeszła (wartość ujemna w string(fromTimeInterval:)), formatter może zwrócić nieprawidłowy ciąg. Zawsze sprawdzaj znak interwału przed przekazaniem do formattera, zwłaszcza przy pracy z danymi serwera, gdzie strefa czasowa może zniekształcić obliczenie.

Według danych Hacker News (2024), jednym z najczęściej dyskutowanych problemów RelativeDateTimeFormatter jest brak wbudowanej obsługi „wczoraj“ i „dzisiaj“ dla języka angielskiego. Zamiast „wczoraj“ formatter dla różnicy 90000 sekund zwróci „1 dzień temu“. Dla języka rosyjskiego takiego problemu nie ma — „1 dzień temu“ brzmi naturalnie, ale dla angielskiego UI „yesterday“ jest preferowane. Ta funkcjonalność nie jest obsługiwana i wymaga ręcznego sprawdzenia przez Calendar.isDateInToday/Yesterday.

Często zadawane pytania

Co to jest RelativeDateTimeFormatter?

RelativeDateTimeFormatter — klasa Foundation do wyświetlania dat w formacie względnym: „5 minut temu“, „za 2 dni“. Dostępna od iOS 13 i macOS 10.15.

Jak RelativeDateTimeFormatter wybiera jednostki?

Według zasady największej niezerowej jednostki — sekundy, minuty, godziny, dni, tygodnie, miesiące lub lata. Na przykład dla różnicy 3720 sekund (1 godzina 2 minuty) zostanie wybrana jednostka „godzina“, a nie „minuty“.

Jak zmienić język wyniku?

Ustaw właściwość locale na odpowiedni egzemplarz Locale. Domyślnie używane jest Locale.current. Przykład: formatter.locale = Locale(identifier: "de_DE") dla języka niemieckiego.

Jaka jest różnica między .numeric a .abbreviated?

.numeric — pełna forma („3 dni temu“), .abbreviated — skrócona („3 dn. temu“). Wybór zależy od kontekstu: numeric dla głównego UI, abbreviated dla kompaktowych elementów.

Jak wyświetlić „właśnie teraz“ zamiast „0 sekund temu“?

Dodaj ręczne sprawdzenie interwału mniejszego niż 5–10 sekund. RelativeDateTimeFormatter nie obsługuje „właśnie teraz“ — dla małych interwałów zwraca „0 sekund temu“. Użyj logiki warunkowej z progiem.

Podsumowanie

  • RelativeDateTimeFormatter — wygodna klasa do wyświetlania dat względnych w iOS 13+
  • Automatyczna lokalizacja — poprawne odmiany dla wszystkich obsługiwanych języków przez ICU
  • Trzy style — .numeric (standard), .abbreviated (kompaktowy), .full (słownie)
  • Wybór jednostki — automatyczny według zasady największej niezerowej wartości
  • Konfiguracja TimeZone — obowiązkowa dla spójności przy pracy z datami serwera
  • Próg „właśnie teraz“ — nie obsługiwany wbudowanie; wymagane ręczne sprawdzenie interwału
  • Brak obsługi „wczoraj“ — dla języka angielskiego formatter nie używa formy yesterday

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również