RelativeDateTimeFormatter là một lớp Foundation trong iOS và macOS chuyển đổi ngày tháng tuyệt đối thành các cụm từ tương đối dễ đọc: "5 phút trước", "hôm qua", "trong 3 ngày". Theo Apple Developer Documentation, 2024, RelativeDateTimeFormatter tự động chọn đơn vị phù hợp (giây, phút, giờ, ngày) và bản địa hóa đầu ra theo ngôn ngữ của ngôn ngữ hiện tại của thiết bị. Không giống như tính toán thủ công sự khác biệt giữa các ngày thông qua Calendar, lớp này tính đến các đặc điểm ngôn ngữ của từng ngôn ngữ: đối với một số ngôn ngữ, số từ được biến cách, đối với những ngôn ngữ khác, một dạng đặc biệt cho từ "hôm qua" được sử dụng. Lớp này có sẵn từ iOS 13 và macOS 10.15.
Những điểm chính
RelativeDateTimeFormatter là một lớp con của Formatter trong Foundation nhận một Date (hoặc chênh lệch tính bằng giây) và trả về một chuỗi đã được bản địa hóa với thời gian tương đối. Ví dụ: cho một ngày trước 5 phút so với ngày hiện tại, nó trả về "5 phút trước". Lớp này hỗ trợ ba ngữ cảnh thời gian: quá khứ, tương lai và hiện tại.
Logic nội bộ của RelativeDateTimeFormatter sử dụng Calendar và Locale để tính toán chênh lệch giữa các ngày và chọn dạng ngữ pháp chính xác. Đối với tiếng Việt, nó chọn giữa "1 phút trước" và "5 phút trước". Chức năng này dựa trên dữ liệu ICU (International Components for Unicode) và không yêu cầu cấu hình bổ sung từ nhà phát triển.
Theo Apple WWDC 2019, RelativeDateTimeFormatter đã trở thành một phần của framework để đơn giản hóa việc bản địa hóa — trước khi nó ra đời, các nhà phát triển phải tính toán thủ công chênh lệch ngày và thay thế các chuỗi đã bản địa hóa thông qua String.localizedStringWithFormat. Điều này dẫn đến lỗi biến cách (đặc biệt đối với các ngôn ngữ Slav và Ả Rập) và lựa chọn sai đơn vị đo lường.
Thuật toán của RelativeDateTimeFormatter bao gồm ba bước: tính toán chênh lệch giữa ngày được truyền và thời điểm hiện tại, chọn đơn vị thích hợp (lớn nhất không cho kết quả bằng 0) và định dạng theo ngôn ngữ. Ví dụ: đối với chênh lệch 3720 giây (1 giờ 2 phút), đơn vị "giờ" được chọn và kết quả là "1 giờ trước", không phải "62 phút trước".
Các đơn vị được chọn theo nguyên tắc "lớn nhất khác không": nếu chênh lệch lớn hơn 86400 giây (1 ngày), ngày được sử dụng; nếu lớn hơn 604800 (1 tuần) — tuần, v.v. Thuật toán này đảm bảo kết quả luôn dễ đọc một cách tự nhiên: thay vì "518400 giây trước", người dùng thấy "6 ngày trước". Ranh giới chính xác của các đơn vị được xác định bởi lịch của ngôn ngữ hiện tại.
| Phạm vi chênh lệch | Đơn vị | Ví dụ cho vi_VN |
|---|---|---|
| 0–59 giây | Seconds | 30 giây trước |
| 1–59 phút | Minutes | 5 phút trước |
| 1–23 giờ | Hours | 3 giờ trước |
| 1–6 ngày | Days | 2 ngày trước |
| 7–27 ngày | Weeks | 1 tuần trước |
| 28 ngày–11 tháng | Months | 3 tháng trước |
| 12+ tháng | Years | 1 năm trước |
Ngữ cảnh định dạng xác định phần cuối của cụm từ. Đối với quá khứ: "trước" (tiếng Việt). Đối với tương lai: "trong 3 ngày" (tiếng Việt). Đối với hiện tại: "bây giờ" (tiếng Việt). Ngữ cảnh được đặt thông qua phương thức localizeString(fromTimeInterval:) hoặc trực tiếp thông qua string(from: Date).
RelativeDateTimeFormatter cung cấp một số cài đặt để kiểm soát đầu ra: thuộc tính unitsStyle xác định kiểu định dạng (numeric, abbreviated, full, spellOut) và maximumUnitCount giới hạn số lượng đơn vị được hiển thị. Ví dụ: với maximumUnitCount = 1, chênh lệch 1 giờ 30 phút được hiển thị là "1 giờ trước" thay vì "1 giờ 30 phút trước".
Giới hạn đơn vị: theo mặc định, RelativeDateTimeFormatter chỉ hiển thị một đơn vị (lớn nhất). Đặt maximumUnitCount = 2 sẽ bao gồm đơn vị tiếp theo để mô tả chính xác hơn: "1 giờ 30 phút trước". Tuy nhiên, điều này có thể làm cho chuỗi quá dài đối với các tin nhắn ngắn (thông báo đẩy, cảnh báo). Đối với UI, khuyến nghị giữ 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()))")
Chọn kiểu cho các ngữ cảnh khác nhau: cho bảng tin, sử dụng .numeric với maximumUnitCount = 1 — đây là tiêu chuẩn cho Twitter, Instagram và Facebook. Cho khả năng tiếp cận (VoiceOver), sử dụng .full — các số được đánh vần sẽ được đọc tự nhiên hơn. Cho các phần tử nhỏ gọn (huy hiệu thông báo, thanh trạng thái), sử dụng .abbreviated để tiết kiệm không gian.
Sử dụng cơ bản của RelativeDateTimeFormatter bao gồm tạo một phiên bản, cấu hình thuộc tính và gọi một trong các phương thức định dạng. Các phương thức chính là: localizedString(for:relativeTo:) — cho một cặp ngày, localizedString(fromTimeInterval:) — cho chênh lệch tính bằng giây, và string(for:) — cho Date với ngữ cảnh tự động (quá khứ/tương lai).
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)")
Xử lý tình huống "vừa mới" — RelativeDateTimeFormatter không có hỗ trợ tích hợp cho cụm từ "vừa mới" đối với các khoảng thời gian rất nhỏ. Đối với chênh lệch dưới 5 giây, nó trả về "0 giây trước", trông không đẹp trong UI. Khuyến nghị bọc lời gọi trình định dạng trong logic có điều kiện: nếu chênh lệch nhỏ hơn ngưỡng đã đặt (ví dụ: 5 giây) — hiển thị thủ công "vừa mới", nếu không thì chuyển ngày cho trình định dạng.
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)))
Phương thức string(fromTimeInterval:) chấp nhận chênh lệch tính bằng giây và tự động xác định ngữ cảnh (giá trị dương — tương lai, âm — quá khứ). Điều này thuận tiện khi chênh lệch đã được biết (ví dụ: nhận được từ máy chủ dưới dạng dấu thời gian unix). Trong trường hợp này, không cần tạo Date — chênh lệch được truyền trực tiếp.
RelativeDateTimeFormatter tự động bản địa hóa đầu ra dựa trên Locale.current. Để thay đổi ngôn ngữ định dạng, hãy đặt thuộc tính locale — không giống như DateFormatter, đối với RelativeDateTimeFormatter, ngôn ngữ không cố định và có thể được thay đổi cho mỗi lần gọi. Điều này cho phép hiển thị ngày tương đối bằng ngôn ngữ khác với ngôn ngữ giao diện (ví dụ: nội dung bằng ngôn ngữ gốc).
Sự phức tạp của việc bản địa hóa ngày tương đối nằm ở các đặc điểm ngữ pháp của các ngôn ngữ khác nhau. Tiếng Việt yêu cầu các dạng số từ khác nhau: "1 phút", "2 phút". Tiếng Ả Rập sử dụng dạng số nhiều cho các số từ 3 đến 10 và các dạng đặc biệt cho 11+. Tiếng Trung không có biến cách nào, điều này đơn giản hóa nhiệm vụ. RelativeDateTimeFormatter bao gồm tất cả các trường hợp này thông qua các quy tắc ICU, mà không cần mã bổ sung.
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 Vietnamese 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()))")
}
Một sắc thái quan trọng: RelativeDateTimeFormatter bỏ qua TimeZone khi tính toán chênh lệch cho cài đặt .numeric — nó sử dụng chênh lệch tuyệt đối tính bằng giây. Tuy nhiên, đối với kiểu .full (với các số được đánh vần) và các trường hợp đặc biệt (hôm qua, hôm nay), TimeZone được tính đến. Luôn đặt TimeZone một cách rõ ràng để đảm bảo tính nhất quán, đặc biệt nếu ứng dụng làm việc với ngày tháng từ máy chủ theo UTC.
Bỏ qua TimeZone khi tính ngày tương đối — một lỗi phổ biến khi làm việc với ngày tháng từ máy chủ. Nếu máy chủ gửi Date theo UTC và RelativeDateTimeFormatter sử dụng TimeZone.current, chênh lệch có thể được tính không chính xác cho các ngày gần với thời điểm hiện tại. Khuyến nghị luôn đặt formatter.timeZone = TimeZone(secondsFromGMT: 0) cho dữ liệu máy chủ.
Chọn sai đơn vị cho khoảng thời gian ngắn — RelativeDateTimeFormatter làm tròn chênh lệch đến đơn vị lớn nhất. Đối với 25 giờ, kết quả sẽ là "1 ngày trước", điều này có thể gây hiểu lầm cho người dùng. Nếu cần độ chính xác cao (ví dụ: cho bộ đếm thời gian đếm ngược), hãy sử dụng DateComponentsFormatter thay vì RelativeDateTimeFormatter — nó cho phép hiển thị nhiều đơn vị cùng một lúc.
Thiếu kiểm tra TimeInterval âm — nếu một ngày trong tương lai được truyền như quá khứ (giá trị âm trong string(fromTimeInterval:)), trình định dạng có thể trả về một chuỗi không chính xác. Luôn kiểm tra dấu của khoảng thời gian trước khi truyền nó cho trình định dạng, đặc biệt khi làm việc với dữ liệu máy chủ nơi múi giờ có thể làm sai lệch tính toán.
Theo Hacker News (2024), một trong những vấn đề được thảo luận nhiều nhất của RelativeDateTimeFormatter là thiếu hỗ trợ tích hợp cho "hôm qua" và "hôm nay" đối với tiếng Anh. Thay vì "hôm qua", trình định dạng cho chênh lệch 90000 giây trả về "1 ngày trước". Đối với tiếng Việt, không có vấn đề như vậy — "1 ngày trước" nghe tự nhiên, nhưng đối với UI tiếng Anh, "yesterday" được ưa thích hơn. Chức năng này không được hỗ trợ và yêu cầu kiểm tra thủ công thông qua Calendar.isDateInToday/Yesterday.
Câu hỏi thường gặp
RelativeDateTimeFormatter là một lớp Foundation để hiển thị ngày ở định dạng tương đối: "5 phút trước", "trong 2 ngày". Có sẵn từ iOS 13 và macOS 10.15.
Theo nguyên tắc đơn vị lớn nhất khác không — giây, phút, giờ, ngày, tuần, tháng hoặc năm. Ví dụ: đối với chênh lệch 3720 giây (1 giờ 2 phút), đơn vị "giờ" được chọn, không phải "phút".
Đặt thuộc tính locale thành phiên bản Locale mong muốn. Theo mặc định, Locale.current được sử dụng. Ví dụ: formatter.locale = Locale(identifier: "de_DE") cho tiếng Đức.
.numeric — dạng đầy đủ ("3 ngày trước"), .abbreviated — dạng rút gọn ("3 ng. trước"). Lựa chọn phụ thuộc vào ngữ cảnh: numeric cho UI chính, abbreviated cho các phần tử nhỏ gọn.
Thêm kiểm tra thủ công cho khoảng thời gian dưới 5-10 giây. RelativeDateTimeFormatter không hỗ trợ "vừa mới" — đối với khoảng thời gian nhỏ, nó trả về "0 giây trước". Sử dụng logic có điều kiện với ngưỡng.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm