RelativeDateTimeFormatter este o clasă Foundation în iOS și macOS care transformă datele absolute în formulări relative lizibile pentru om: „acum 5 minute“, „ieri“, „peste 3 zile“. Conform Documentației pentru dezvoltatori Apple, 2024, RelativeDateTimeFormatter selectează automat unitatea potrivită (secunde, minute, ore, zile) și localizează rezultatul în limba localei curente a dispozitivului. Spre deosebire de calcularea manuală a diferenței dintre date prin Calendar, această clasă ține cont de particularitățile lingvistice ale fiecărei limbi: pentru unele limbi numeralele se declină, pentru altele — se folosește o formă specială pentru cuvântul „ieri“. Clasa este disponibilă începând cu iOS 13 și macOS 10.15.
Esențialul
RelativeDateTimeFormatter este o subclasă a Formatter în Foundation care primește Date (sau diferența în secunde) și returnează un șir localizat cu timp relativ. De exemplu, pentru o dată cu 5 minute mai devreme decât cea curentă va returna „acum 5 minute“ pentru ru_RU sau „5 minutes ago“ pentru en_US. Clasa suportă trei contexte temporale: trecut (past), viitor (future) și prezent (present).
Logica internă a RelativeDateTimeFormatter folosește Calendar și Locale pentru a calcula diferența dintre date și a alege forma gramaticală corectă. Pentru limba română clasa declină corect numeralele: „acum 1 minut“, „acum 2 minute“, „acum 5 minute“. Pentru engleză — alege între „minute ago“ și „minutes ago“. Această funcționalitate se bazează pe datele ICU (International Components for Unicode) și nu necesită configurare suplimentară din partea dezvoltatorului.
Conform Apple WWDC 2019, RelativeDateTimeFormatter a devenit parte a framework-ului pentru simplificarea localizării — înainte de apariția sa dezvoltatorii erau nevoiți să calculeze manual diferența datelor și să insereze șiruri localizate prin String.localizedStringWithFormat. Aceasta ducea la erori de declinare (mai ales pentru limbile slave și arabe) și la selectarea incorectă a unităților de măsură.
Algoritmul de funcționare RelativeDateTimeFormatter constă din trei pași: calcularea diferenței dintre data transmisă și momentul curent, selectarea unității potrivite (cea mai mare care nu dă zero) și formatarea cu luarea în considerare a localei. De exemplu, pentru o diferență de 3720 de secunde (1 oră 2 minute) va fi selectată unitatea „oră“, iar rezultatul va fi „acum 1 oră“, nu „acum 62 de minute“.
Unitățile sunt selectate după principiul „celei mai mari nenule“: dacă diferența este mai mare de 86400 de secunde (1 zi), se folosesc zilele; dacă mai mare de 604800 (1 săptămână) — săptămânile și așa mai departe. Acest algoritm garantează că rezultatul este mereu citit natural: în loc de „acum 518400 de secunde“ utilizatorul vede „acum 6 zile“. Limitele exacte ale unităților sunt determinate de calendarul localei curente.
| Intervalul diferenței | Unitatea | Exemplu pentru ru_RU |
|---|---|---|
| 0–59 de secunde | Seconds | acum 30 de secunde |
| 1–59 de minute | Minutes | acum 5 minute |
| 1–23 de ore | Hours | acum 3 ore |
| 1–6 zile | Days | acum 2 zile |
| 7–27 de zile | Weeks | acum 1 săptămână |
| 28 de zile–11 luni | Months | acum 3 luni |
| 12+ luni | Years | acum 1 an |
Contextul de formatare determină finalul frazei. Pentru trecut: „în urmă“ (română), „ago“ (engleză). Pentru viitor: „peste 3 zile“ (română), „in 3 days“ (engleză). Pentru prezent: „acum“ (română), „now“ (engleză). Contextul este setat prin metoda localizeString(fromTimeInterval:) sau direct prin string(from: Date).
RelativeDateTimeFormatter oferă mai multe setări pentru controlul rezultatului: proprietatea unitsStyle determină stilul de formatare (numeric, abbreviated, full, spellOut), iar maximumUnitCount limitează numărul de unități afișate. De exemplu, cu maximumUnitCount = 1 o diferență de 1 oră 30 de minute va fi afișată ca „acum 1 oră“ în loc de „acum 1 oră 30 de minute“.
Limitarea unităților: în mod implicit RelativeDateTimeFormatter afișează doar o singură (cea mai mare) unitate. Setarea maximumUnitCount = 2 activează următoarea unitate pentru o descriere mai precisă: „acum 1 oră 30 de minute“. Totuși, aceasta poate face șirul excesiv de lung pentru mesaje scurte (push, notificări). Pentru UI se recomandă păstrarea maximumUnitCount = 1.
import Foundation
let formatter = RelativeDateTimeFormatter()
// Configurați stilurile
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Exemple cu date diferite
let fiveMinAgo = Date().addingTimeInterval(-300)
print("acum 5 min: \(formatter.localizedString(for: fiveMinAgo, relativeTo: Date()))")
let twoDaysLater = Date().addingTimeInterval(172800)
print("peste 2 zile: \(formatter.localizedString(for: twoDaysLater, relativeTo: Date()))")
// Stil prescurtat
formatter.unitsStyle = .abbreviated
let oneWeekAgo = Date().addingTimeInterval(-604800)
print("Prescurtat: \(formatter.localizedString(for: oneWeekAgo, relativeTo: Date()))")
// Stil complet (în litere)
formatter.unitsStyle = .full
let threeHours = Date().addingTimeInterval(10800)
print("Complet: \(formatter.localizedString(for: threeHours, relativeTo: Date()))")
Selectarea stilului pentru diferite contexte: pentru fluxul de știri folosiți .numeric cu maximumUnitCount = 1 — este standardul pentru Twitter, Instagram și Facebook. Pentru Accessibility (VoiceOver) folosiți .full — numerele în litere se citesc mai natural. Pentru elementele compacte (colț de notificare, bara de status) folosiți .abbreviated pentru a economisi spațiu.
Utilizarea de bază a RelativeDateTimeFormatter se reduce la crearea unei instanțe, configurarea proprietăților și apelarea uneia dintre metodele de formatare. Metodele principale: localizedString(for:relativeTo:) — pentru o pereche de date, localizedString(fromTimeInterval:) — pentru diferența în secunde, și string(for:) — pentru Date cu context automat (trecut/viitor).
import Foundation
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Exemple de UI pentru rețele sociale
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)")
}
// Date viitoare
let reminderFormatter = RelativeDateTimeFormatter()
reminderFormatter.unitsStyle = .abbreviated
let inOneHour = Date().addingTimeInterval(3600)
let reminderText = reminderFormatter.localizedString(
for: inOneHour,
relativeTo: Date()
)
print("Memento: \(reminderText)")
Tratarea scenariului „chiar acum“ — RelativeDateTimeFormatter nu are suport integrat pentru expresia „chiar acum“ pentru intervale foarte mici. Pentru o diferență mai mică de 5 secunde va returna „acum 0 secunde“, ceea ce arată urât în UI. Se recomandă împachetarea apelului formatter-ului în logică condițională: dacă diferența este mai mică de pragul stabilit (de exemplu, 5 secunde) — afișați „chiar acum“ manual, altfel transmiteți data către formatter.
import Foundation
func relativeTimeString(from date: Date) -> String {
let interval = Date().timeIntervalSince(date)
// Pragul „chiar acum“
if interval < 5 {
return "just now"
}
// Pragul „azi“
if interval < 60 {
return "just now"
}
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Afișare fără sufixul „în urmă“
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:) primește diferența în secunde și determină automat contextul (valoare pozitivă — viitor, negativă — trecut). Este convenabil atunci când diferența este deja cunoscută (de exemplu, primită de la server ca unix timestamp). În acest caz nu este nevoie să creați Date — diferența este transmisă direct.
RelativeDateTimeFormatter localizează automat rezultatul pe baza Locale.current. Pentru a schimba limba de formatare setați proprietatea locale — spre deosebire de DateFormatter, pentru RelativeDateTimeFormatter locale nu este fixată și poate fi schimbată pentru fiecare apel. Acest lucru permite afișarea datelor relative într-o limbă diferită de limba interfeței (de exemplu, conținut în limba originală).
Complexitatea localizării datelor relative constă în particularitățile gramaticale ale diferitelor limbi. Limba rusă necesită forme diferite ale numeralelor: „1 minut“, „2 minute“, „5 minute“. Araba — folosește forma de plural pentru numerele de la 3 la 10 și forme speciale pentru 11+. Chineza — nu are deloc declinare, ceea ce simplifică sarcina. RelativeDateTimeFormatter acoperă toate aceste cazuri prin regulile ICU, fără necesitatea unui cod suplimentar.
import Foundation
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
let targetDate = Date().addingTimeInterval(-7200) // acum 2 ore
// Locali diferite
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)")
}
// Verificați declinarea rusă
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()))")
}
Un detaliu important: RelativeDateTimeFormatter ignoră TimeZone la calcularea diferenței pentru setările .numeric — folosește diferența absolută în secunde. Totuși, pentru stilul .full (cu numere în litere) și cazurile speciale (ieri, azi) TimeZone este luat în considerare. Setați întotdeauna TimeZone explicit pentru consistență, mai ales dacă aplicația lucrează cu date de server în UTC.
Ignorarea TimeZone la calcularea datelor relative — o eroare frecventă la lucrul cu datele de server. Dacă serverul trimite Date în UTC, iar RelativeDateTimeFormatter folosește TimeZone.current, diferența poate fi calculată incorect pentru datele apropiate de momentul curent. Se recomandă să setați întotdeauna formatter.timeZone = TimeZone(secondsFromGMT: 0) pentru datele de server.
Selectarea incorectă a unității pentru intervale scurte — RelativeDateTimeFormatter rotunjește diferența la cea mai mare unitate. Pentru 25 de ore rezultatul va fi „acum 1 zi“, ceea ce poate induce utilizatorul în eroare. Dacă este necesară o precizie ridicată (de exemplu, pentru cronometre de numărătoare inversă), folosiți DateComponentsFormatter în loc de RelativeDateTimeFormatter — acesta permite afișarea mai multor unități simultan.
Lipsa verificării TimeInterval negativ — dacă o dată din viitor este transmisă ca trecută (valoare negativă în string(fromTimeInterval:)), formatter-ul poate returna un șir incorect. Verificați întotdeauna semnul intervalului înainte de transmiterea către formatter, mai ales la lucrul cu datele de server, unde fusul orar poate distorsiona calculul.
Conform Hacker News (2024), una dintre cele mai discutate probleme ale RelativeDateTimeFormatter este lipsa suportului integrat pentru „ieri“ și „azi“ în limba engleză. În loc de „ieri“, formatter-ul pentru o diferență de 90000 de secunde va returna „acum 1 zi“. Pentru limba rusă nu există o astfel de problemă — „acum 1 zi“ sună natural, dar pentru UI-ul în engleză „yesterday“ este preferabil. Această funcționalitate nu este suportată și necesită verificare manuală prin Calendar.isDateInToday/Yesterday.
Întrebări frecvente
RelativeDateTimeFormatter — clasă Foundation pentru afișarea datelor în format relativ: „acum 5 minute“, „peste 2 zile“. Disponibil din iOS 13 și macOS 10.15.
După principiul celei mai mari unități nenule — secunde, minute, ore, zile, săptămâni, luni sau ani. De exemplu, pentru diferența de 3720 de secunde (1 oră 2 minute) va fi selectată unitatea „oră“, nu „minute“.
Setați proprietatea locale pe instanța Locale dorită. În mod implicit este folosit Locale.current. Exemplu: formatter.locale = Locale(identifier: "de_DE") pentru limba germană.
.numeric — forma completă („acum 3 zile“), .abbreviated — prescurtată („acum 3 zile“). Alegerea depinde de context: numeric pentru UI-ul principal, abbreviated pentru elementele compacte.
Adăugați o verificare manuală pentru un interval mai mic de 5–10 secunde. RelativeDateTimeFormatter nu suportă „chiar acum“ — pentru intervale mici returnează „acum 0 secunde“. Folosiți logică condițională cu prag.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și