RelativeDateTimeFormatter è una classe Foundation in iOS e macOS che converte date assolute in frasi relative leggibili dall'uomo: "5 minuti fa", "ieri", "tra 3 giorni". Secondo Apple Developer Documentation, 2024, RelativeDateTimeFormatter seleziona automaticamente l'unità appropriata (secondi, minuti, ore, giorni) e localizza l'output nella lingua delle impostazioni internazionali correnti del dispositivo. A differenza del calcolo manuale della differenza tra le date tramite Calendar, questa classe tiene conto delle caratteristiche linguistiche di ogni lingua: per alcune lingue i numerali si declinano, per altre viene usata una forma speciale per la parola "ieri". La classe è disponibile a partire da iOS 13 e macOS 10.15.
Punti chiave
RelativeDateTimeFormatter è una sottoclasse di Formatter in Foundation che prende una Date (o una differenza in secondi) e restituisce una stringa localizzata con tempo relativo. Ad esempio, per una data 5 minuti prima di quella corrente, restituisce "5 minuti fa". La classe supporta tre contesti temporali: passato, futuro e presente.
La logica interna di RelativeDateTimeFormatter utilizza Calendar e Locale per calcolare la differenza tra le date e selezionare la forma grammaticale corretta. Per l'italiano, sceglie tra "1 minuto fa" e "5 minuti fa". Questa funzionalità si basa sui dati ICU (International Components for Unicode) e non richiede configurazione aggiuntiva da parte dello sviluppatore.
Secondo Apple WWDC 2019, RelativeDateTimeFormatter è diventato parte del framework per semplificare la localizzazione — prima della sua introduzione, gli sviluppatori dovevano calcolare manualmente la differenza delle date e sostituire le stringhe localizzate tramite String.localizedStringWithFormat. Ciò causava errori di declinazione (specialmente per le lingue slave e arabe) e una selezione errata delle unità di misura.
L'algoritmo di RelativeDateTimeFormatter consiste in tre passaggi: calcolare la differenza tra la data passata e il momento corrente, selezionare l'unità appropriata (la più grande che non dia zero) e formattare in base alle impostazioni internazionali. Ad esempio, per una differenza di 3720 secondi (1 ora e 2 minuti), viene selezionata l'unità "ora" e il risultato è "1 ora fa", non "62 minuti fa".
Le unità vengono selezionate secondo il principio della "più grande non nulla": se la differenza è maggiore di 86400 secondi (1 giorno), vengono usati i giorni; se maggiore di 604800 (1 settimana) — le settimane, e così via. Questo algoritmo garantisce che il risultato sia sempre letto naturalmente: invece di "518400 secondi fa", l'utente vede "6 giorni fa". I confini esatti delle unità sono determinati dal calendario delle impostazioni internazionali correnti.
| Intervallo di differenza | Unità | Esempio per it_IT |
|---|---|---|
| 0–59 secondi | Seconds | 30 secondi fa |
| 1–59 minuti | Minutes | 5 minuti fa |
| 1–23 ore | Hours | 3 ore fa |
| 1–6 giorni | Days | 2 giorni fa |
| 7–27 giorni | Weeks | 1 settimana fa |
| 28 giorni–11 mesi | Months | 3 mesi fa |
| 12+ mesi | Years | 1 anno fa |
Il contesto di formattazione determina la terminazione della frase. Per il passato: "fa" (italiano). Per il futuro: "tra 3 giorni" (italiano). Per il presente: "ora" (italiano). Il contesto viene impostato tramite il metodo localizeString(fromTimeInterval:) o direttamente tramite string(from: Date).
RelativeDateTimeFormatter fornisce diverse impostazioni per controllare l'output: la proprietà unitsStyle determina lo stile di formattazione (numeric, abbreviated, full, spellOut), e maximumUnitCount limita il numero di unità visualizzate. Ad esempio, con maximumUnitCount = 1, una differenza di 1 ora e 30 minuti viene mostrata come "1 ora fa" invece di "1 ora e 30 minuti fa".
Limitazione delle unità: per impostazione predefinita, RelativeDateTimeFormatter mostra solo un'unità (la più grande). Impostare maximumUnitCount = 2 include l'unità successiva per una descrizione più precisa: "1 ora e 30 minuti fa". Tuttavia, ciò può rendere la stringa eccessivamente lunga per messaggi brevi (notifiche push, avvisi). Per l'interfaccia utente, si raccomanda di mantenere maximumUnitCount = 1.
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()))")
Scelta dello stile per contesti diversi: per un feed di notizie, usa .numeric con maximumUnitCount = 1 — questo è lo standard per Twitter, Instagram e Facebook. Per l'Accessibilità (VoiceOver), usa .full — i numeri scritti per esteso vengono letti in modo più naturale. Per elementi compatti (badge di notifica, barra di stato), usa .abbreviated per risparmiare spazio.
Uso base di RelativeDateTimeFormatter si riduce alla creazione di un'istanza, alla configurazione delle proprietà e alla chiamata di uno dei metodi di formattazione. I metodi principali sono: localizedString(for:relativeTo:) — per una coppia di date, localizedString(fromTimeInterval:) — per una differenza in secondi, e string(for:) — per Date con contesto automatico (passato/futuro).
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)")
Gestione dello scenario "appena adesso" — RelativeDateTimeFormatter non ha supporto integrato per la frase "appena adesso" per intervalli molto piccoli. Per una differenza inferiore a 5 secondi, restituisce "0 secondi fa", che non è gradevole nell'interfaccia utente. Si consiglia di avvolgere la chiamata al formattatore in una logica condizionale: se la differenza è inferiore a una soglia impostata (ad esempio, 5 secondi) — mostra manualmente "appena adesso", altrimenti passa la data al formattatore.
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)))
Il metodo string(fromTimeInterval:) accetta una differenza in secondi e determina automaticamente il contesto (valore positivo — futuro, negativo — passato). Questo è comodo quando la differenza è già nota (ad esempio, ricevuta dal server come timestamp unix). In questo caso, non è necessario creare una Date — la differenza viene passata direttamente.
RelativeDateTimeFormatter localizza automaticamente l'output in base a Locale.current. Per cambiare la lingua di formattazione, imposta la proprietà locale — a differenza di DateFormatter, per RelativeDateTimeFormatter le impostazioni internazionali non sono fisse e possono essere modificate per ogni chiamata. Ciò consente di visualizzare date relative in una lingua diversa dalla lingua dell'interfaccia (ad esempio, contenuto nella lingua originale).
La complessità della localizzazione delle date relative risiede nelle caratteristiche grammaticali delle diverse lingue. L'italiano richiede diverse forme di numerali: "1 minuto", "2 minuti". L'arabo usa la forma plurale per i numeri da 3 a 10 e forme speciali per 11+. Il cinese non ha declinazioni, semplificando il compito. RelativeDateTimeFormatter copre tutti questi casi attraverso le regole ICU, senza necessità di codice aggiuntivo.
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 Italian 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()))")
}
Una sfumatura importante: RelativeDateTimeFormatter ignora TimeZone nel calcolo della differenza per le impostazioni .numeric — usa la differenza assoluta in secondi. Tuttavia, per lo stile .full (con numeri per esteso) e i casi speciali (ieri, oggi), TimeZone viene considerato. Imposta sempre TimeZone esplicitamente per coerenza, specialmente se l'applicazione lavora con date del server in UTC.
Ignorare TimeZone nel calcolo delle date relative — un errore comune quando si lavora con date del server. Se il server invia una Date in UTC e RelativeDateTimeFormatter usa TimeZone.current, la differenza potrebbe essere calcolata in modo errato per date vicine al momento corrente. Si raccomanda di impostare sempre formatter.timeZone = TimeZone(secondsFromGMT: 0) per i dati del server.
Selezione errata dell'unità per intervalli brevi — RelativeDateTimeFormatter arrotonda la differenza all'unità più grande. Per 25 ore, il risultato sarà "1 giorno fa", il che può trarre in inganno l'utente. Se è necessaria un'elevata precisione (ad esempio, per timer di conto alla rovescia), usa DateComponentsFormatter invece di RelativeDateTimeFormatter — consente di visualizzare più unità contemporaneamente.
Mancata verifica del TimeInterval negativo — se una data futura viene passata come passata (valore negativo in string(fromTimeInterval:)), il formattatore potrebbe restituire una stringa errata. Controlla sempre il segno dell'intervallo prima di passarlo al formattatore, specialmente quando lavori con dati del server dove il fuso orario potrebbe distorcere il calcolo.
Secondo Hacker News (2024), uno dei problemi più discussi di RelativeDateTimeFormatter è la mancanza di supporto integrato per "ieri" e "oggi" per la lingua inglese. Invece di "ieri", il formattatore per una differenza di 90000 secondi restituisce "1 giorno fa". Per l'italiano non esiste questo problema — "1 giorno fa" suona naturale, ma per l'interfaccia utente inglese "yesterday" è preferibile. Questa funzionalità non è supportata e richiede una verifica manuale tramite Calendar.isDateInToday/Yesterday.
Domande frequenti
RelativeDateTimeFormatter è una classe Foundation per visualizzare le date in formato relativo: "5 minuti fa", "tra 2 giorni". Disponibile da iOS 13 e macOS 10.15.
Secondo il principio dell'unità non nulla più grande — secondi, minuti, ore, giorni, settimane, mesi o anni. Ad esempio, per una differenza di 3720 secondi (1 ora e 2 minuti), viene selezionata l'unità "ora", non "minuti".
Imposta la proprietà locale sull'istanza Locale desiderata. Per impostazione predefinita viene usato Locale.current. Esempio: formatter.locale = Locale(identifier: "de_DE") per il tedesco.
.numeric — forma completa ("3 giorni fa"), .abbreviated — forma abbreviata ("3 gg. fa"). La scelta dipende dal contesto: numeric per l'interfaccia utente principale, abbreviated per elementi compatti.
Aggiungi un controllo manuale per un intervallo inferiore a 5-10 secondi. RelativeDateTimeFormatter non supporta "appena adesso" — per piccoli intervalli restituisce "0 secondi fa". Usa logica condizionale con una soglia.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche