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 — 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.
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óżnicy | Jednostka | Przykład dla ru_RU |
|---|---|---|
| 0–59 sekund | Seconds | 30 sekund temu |
| 1–59 minut | Minutes | 5 minut temu |
| 1–23 godzin | Hours | 3 godziny temu |
| 1–6 dni | Days | 2 dni temu |
| 7–27 dni | Weeks | 1 tydzień temu |
| 28 dni–11 miesięcy | Months | 3 miesiące temu |
| 12+ miesięcy | Years | 1 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).
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“.
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.
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.
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ść).
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.
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.
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.
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.
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
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.
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“.
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.
.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.
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
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.
Przeczytaj również