Το RelativeDateTimeFormatter είναι μια κλάση Foundation στο iOS και το macOS που μετατρέπει τις απόλυτες ημερομηνίες σε αναγνώσιμες σχετικές διατυπώσεις: “5 λεπτά πριν”, “χθες”, “σε 3 ημέρες”. Σύμφωνα με το Apple Developer Documentation, 2024, το RelativeDateTimeFormatter επιλέγει αυτόματα την κατάλληλη μονάδα (δευτερόλεπτα, λεπτά, ώρες, ημέρες) και εντοπίζει την έξοδο στη γλώσσα της τρέχουσας τοποθεσίας της συσκευής. Σε αντίθεση με τον χειροκίνητο υπολογισμό της διαφοράς μεταξύ ημερομηνιών μέσω του Calendar, αυτή η κλάση λαμβάνει υπόψη τα γλωσσικά χαρακτηριστικά κάθε γλώσσας: για ορισμένες γλώσσες τα αριθμητικά κλίνονται, για άλλες — χρησιμοποιείται ειδικός τύπος για τη λέξη “χθες”. Η κλάση διατίθεται από το iOS 13 και το macOS 10.15.
Το βασικό
RelativeDateTimeFormatter είναι μια υποκλάση του Formatter στο Foundation που δέχεται ένα Date (ή τη διαφορά σε δευτερόλεπτα) και επιστρέφει μια εντοπισμένη συμβολοσειρά με σχετικό χρόνο. Για παράδειγμα, για μια ημερομηνία 5 λεπτά νωρίτερα από την τρέχουσα, θα επιστρέψει “5 λεπτά πριν” για το ru_RU ή “5 minutes ago” για το en_US. Η κλάση υποστηρίζει τρία χρονικά πλαίσια: παρελθόν (past), μέλλον (future) και παρόν (present).
Η εσωτερική λογική του RelativeDateTimeFormatter χρησιμοποιεί το Calendar και το Locale για τον υπολογισμό της διαφοράς μεταξύ ημερομηνιών και την επιλογή της σωστής γραμματικής μορφής. Για τα ελληνικά, η κλάση κλίνει σωστά τα αριθμητικά: “1 λεπτό πριν”, “2 λεπτά πριν”, “5 λεπτά πριν”. Για τα αγγλικά — επιλέγει μεταξύ “minute ago” και “minutes ago”. Αυτή η λειτουργία βασίζεται στα δεδομένα ICU (International Components for Unicode) και δεν απαιτεί πρόσθετη ρύθμιση από τον προγραμματιστή.
Σύμφωνα με το Apple WWDC 2019, το RelativeDateTimeFormatter έγινε μέρος του πλαισίου για την απλοποίηση του εντοπισμού — πριν από την εμφάνισή του, οι προγραμματιστές έπρεπε να υπολογίζουν χειροκίνητα τη διαφορά ημερομηνιών και να εισάγουν εντοπισμένες συμβολοσειρές μέσω του String.localizedStringWithFormat. Αυτό οδηγούσε σε σφάλματα κλίσης (ειδικά για τις σλαβικές και αραβικές γλώσσες) και σε εσφαλμένη επιλογή μονάδων μέτρησης.
Ο αλγόριθμος λειτουργίας του RelativeDateTimeFormatter αποτελείται από τρία βήματα: υπολογισμός της διαφοράς μεταξύ της δεδομένης ημερομηνίας και της τρέχουσας στιγμής, επιλογή της κατάλληλης μονάδας (της μεγαλύτερης που δεν δίνει μηδέν) και μορφοποίηση λαμβάνοντας υπόψη την τοποθεσία. Για παράδειγμα, για διαφορά 3720 δευτερολέπτων (1 ώρα 2 λεπτά) θα επιλεγεί η μονάδα “ώρα” και το αποτέλεσμα θα είναι “πριν από 1 ώρα”, όχι “πριν από 62 λεπτά”.
Οι μονάδες επιλέγονται με βάση την αρχή “μεγαλύτερη μη μηδενική”: αν η διαφορά είναι μεγαλύτερη από 86400 δευτερόλεπτα (1 ημέρα), χρησιμοποιούνται ημέρες· αν είναι μεγαλύτερη από 604800 (1 εβδομάδα) — εβδομάδες και ούτω καθεξής. Αυτός ο αλγόριθμος εγγυάται ότι το αποτέλεσμα διαβάζεται πάντα φυσικά: αντί για “πριν από 518400 δευτερόλεπτα”, ο χρήστης βλέπει “πριν από 6 ημέρες”. Τα ακριβή όρια των μονάδων καθορίζονται από το ημερολόγιο της τρέχουσας τοποθεσίας.
| Εύρος διαφοράς | Μονάδα | Παράδειγμα για ru_RU |
|---|---|---|
| 0–59 δευτερόλεπτα | Seconds | πριν από 30 δευτερόλεπτα |
| 1–59 λεπτά | Minutes | πριν από 5 λεπτά |
| 1–23 ώρες | Hours | πριν από 3 ώρες |
| 1–6 ημέρες | Days | πριν από 2 ημέρες |
| 7–27 ημέρες | Weeks | πριν από 1 εβδομάδα |
| 28 ημέρες–11 μήνες | Months | πριν από 3 μήνες |
| 12+ μήνες | Years | πριν από 1 έτος |
Το πλαίσιο μορφοποίησης καθορίζει το τέλος της φράσης. Για το παρελθόν: “πριν” (ελληνικά), “ago” (αγγλικά). Για το μέλλον: “σε 3 ημέρες” (ελληνικά), “in 3 days” (αγγλικά). Για το παρόν: “τώρα” (ελληνικά), “now” (αγγλικά). Το πλαίσιο ορίζεται μέσω της μεθόδου localizeString(fromTimeInterval:) ή απευθείας μέσω του string(from: Date).
RelativeDateTimeFormatter παρέχει διάφορες ρυθμίσεις για τον έλεγχο της εξόδου: η ιδιότητα unitsStyle καθορίζει το στυλ μορφοποίησης (numeric, abbreviated, full, spellOut), και το maximumUnitCount περιορίζει τον αριθμό των εμφανιζόμενων μονάδων. Για παράδειγμα, με maximumUnitCount = 1, μια διαφορά 1 ώρας 30 λεπτών θα εμφανιστεί ως “πριν από 1 ώρα” αντί για “πριν από 1 ώρα 30 λεπτά”.
Περιορισμός μονάδων: από προεπιλογή, το RelativeDateTimeFormatter εμφανίζει μόνο μία (τη μεγαλύτερη) μονάδα. Η ρύθμιση maximumUnitCount = 2 ενεργοποιεί την επόμενη μονάδα για ακριβέστερη περιγραφή: “πριν από 1 ώρα 30 λεπτά”. Ωστόσο, αυτό μπορεί να κάνει τη συμβολοσειρά υπερβολικά μακριά για σύντομα μηνύματα (push, ειδοποιήσεις). Για το UI συνιστάται να διατηρείται maximumUnitCount = 1.
import Foundation
let formatter = RelativeDateTimeFormatter()
// Ρύθμιση στυλ
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Παραδείγματα με διαφορετικές ημερομηνίες
let fiveMinAgo = Date().addingTimeInterval(-300)
print("5 λεπτά πριν: \(formatter.localizedString(for: fiveMinAgo, relativeTo: Date()))")
let twoDaysLater = Date().addingTimeInterval(172800)
print("σε 2 ημέρες: \(formatter.localizedString(for: twoDaysLater, relativeTo: Date()))")
// Συντομευμένο στυλ
formatter.unitsStyle = .abbreviated
let oneWeekAgo = Date().addingTimeInterval(-604800)
print("Συντομευμένο: \(formatter.localizedString(for: oneWeekAgo, relativeTo: Date()))")
// Πλήρες στυλ (με λέξεις)
formatter.unitsStyle = .full
let threeHours = Date().addingTimeInterval(10800)
print("Πλήρες: \(formatter.localizedString(for: threeHours, relativeTo: Date()))")
Επιλογή στυλ για διαφορετικά πλαίσια: για τη ροή ειδήσεων χρησιμοποιήστε .numeric με maximumUnitCount = 1 — αυτό είναι το πρότυπο για Twitter, Instagram και Facebook. Για το Accessibility (VoiceOver) χρησιμοποιήστε .full — τα αριθμητικά με λέξεις διαβάζονται πιο φυσικά. Για συμπαγή στοιχεία (γωνία ειδοποίησης, γραμμή κατάστασης) χρησιμοποιήστε .abbreviated για εξοικονόμηση χώρου.
Βασική χρήση του RelativeDateTimeFormatter συνοψίζεται στη δημιουργία μιας παρουσίας, στη ρύθμιση των ιδιοτήτων και στην κλήση μιας από τις μεθόδους μορφοποίησης. Οι κύριες μέθοδοι: localizedString(for:relativeTo:) — για ένα ζεύγος ημερομηνιών, localizedString(fromTimeInterval:) — για τη διαφορά σε δευτερόλεπτα, και string(for:) — για Date με αυτόματο πλαίσιο (παρελθόν/μέλλον).
import Foundation
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Παραδείγματα UI κοινωνικού δικτύου
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)")
}
// Μελλοντικές ημερομηνίες
let reminderFormatter = RelativeDateTimeFormatter()
reminderFormatter.unitsStyle = .abbreviated
let inOneHour = Date().addingTimeInterval(3600)
let reminderText = reminderFormatter.localizedString(
for: inOneHour,
relativeTo: Date()
)
print("Υπενθύμιση: \(reminderText)")
Διαχείριση του σεναρίου “μόλις τώρα” — το RelativeDateTimeFormatter δεν διαθέτει ενσωματωμένη υποστήριξη για τη φράση “μόλις τώρα” για πολύ μικρά διαστήματα. Για διαφορά μικρότερη από 5 δευτερόλεπτα, θα επιστρέψει “πριν από 0 δευτερόλεπτα”, κάτι που δεν φαίνεται όμορφο στο UI. Συνιστάται να περικλείσετε την κλήση του formatter σε λογική υπό όρους: αν η διαφορά είναι μικρότερη από το καθορισμένο όριο (π.χ. 5 δευτερόλεπτα) — εμφανίστε το “μόλις τώρα” χειροκίνητα, διαφορετικά μεταβιβάστε την ημερομηνία στον formatter.
import Foundation
func relativeTimeString(from date: Date) -> String {
let interval = Date().timeIntervalSince(date)
// Όριο “μόλις τώρα”
if interval < 5 {
return "just now"
}
// Όριο “σήμερα”
if interval < 60 {
return "just now"
}
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
// Εμφάνιση χωρίς επίθημα “πριν”
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, χωρίς πρόσθετο κώδικα.
import Foundation
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .numeric
formatter.maximumUnitCount = 1
let targetDate = Date().addingTimeInterval(-7200) // πριν από 2 ώρες
// Διαφορετικές τοποθεσίες
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)")
}
// Ελέγξτε τη ρωσική κλίση
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)) λεπτά: \(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 — επιτρέπει την εμφάνιση πολλών μονάδων ταυτόχρονα.
Έλλειψη ελέγχου αρνητικού TimeInterval — αν μια μελλοντική ημερομηνία μεταβιβάζεται ως παρελθούσα (αρνητική τιμή στο string(fromTimeInterval:)), ο formatter μπορεί να επιστρέψει εσφαλμένη συμβολοσειρά. Πάντα να ελέγχετε το πρόσημο του διαστήματος πριν από τη μεταβίβαση στον formatter, ειδικά κατά την εργασία με δεδομένα διακομιστή, όπου η ζώνη ώρας μπορεί να παραμορφώσει τον υπολογισμό.
Σύμφωνα με το Hacker News (2024), ένα από τα πιο συζητημένα προβλήματα του RelativeDateTimeFormatter είναι η έλλειψη ενσωματωμένης υποστήριξης για το “χθες” και “σήμερα” στα αγγλικά. Αντί για “χθες”, ο formatter για διαφορά 90000 δευτερολέπτων θα επιστρέψει “πριν από 1 ημέρα”. Για τα ρωσικά δεν υπάρχει τέτοιο πρόβλημα — το “πριν από 1 ημέρα” ακούγεται φυσικό, αλλά για το αγγλικό UI το “yesterday” είναι προτιμότερο. Αυτή η λειτουργία δεν υποστηρίζεται και απαιτεί χειροκίνητο έλεγχο μέσω Calendar.isDateInToday/Yesterday.
Συχνές ερωτήσεις
RelativeDateTimeFormatter — κλάση Foundation για την εμφάνιση ημερομηνιών σε σχετική μορφή: “5 λεπτά πριν”, “σε 2 ημέρες”. Διατίθεται από το iOS 13 και το macOS 10.15.
Με βάση την αρχή της μεγαλύτερης μη μηδενικής μονάδας — δευτερόλεπτα, λεπτά, ώρες, ημέρες, εβδομάδες, μήνες ή έτη. Για παράδειγμα, για διαφορά 3720 δευτερολέπτων (1 ώρα 2 λεπτά) θα επιλεγεί η μονάδα “ώρα”, όχι “λεπτά”.
Ορίστε την ιδιότητα locale στο επιθυμητό στιγμιότυπο Locale. Από προεπιλογή χρησιμοποιείται το Locale.current. Παράδειγμα: formatter.locale = Locale(identifier: "de_DE") για τα γερμανικά.
.numeric — πλήρης μορφή (“πριν από 3 ημέρες”), .abbreviated — συντομευμένη (“πριν από 3 ημέρες”). Η επιλογή εξαρτάται από το πλαίσιο: numeric για το κύριο UI, abbreviated για συμπαγή στοιχεία.
Προσθέστε χειροκίνητο έλεγχο για διάστημα μικρότερο από 5–10 δευτερόλεπτα. Το RelativeDateTimeFormatter δεν υποστηρίζει το “μόλις τώρα” — για μικρά διαστήματα επιστρέφει “πριν από 0 δευτερόλεπτα”. Χρησιμοποιήστε λογική υπό όρους με όριο.
Συμπεράσματα
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης