RelativeDateTimeFormatter: bản chất, ngày tương đối và Swift

Tác giả: IT Sectr Đã đăng: 2026-07-13 Thời gian đọc: 10 phút

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 — một lớp để hiển thị ngày tương đối trong iOS và macOS (iOS 13+)
  • Đầu ra được bản địa hóa — tự động chọn cụm từ theo ngôn ngữ hiện tại
  • Ba loại ngữ cảnh — quá khứ (trước), tương lai (trong), hiện tại (bây giờ) với các cụm từ khác nhau
  • Tự động chọn đơn vị — giây, phút, giờ, ngày, tuần, tháng, năm
  • Tùy chỉnh kiểu — numeric (trong 3 ngày) hoặc abbreviated (trong 3 ng.)

RelativeDateTimeFormatter là gì?

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.

RelativeDateTimeFormatter hiển thị "5 phút trước" như thế nào?

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âySeconds30 giây trước
1–59 phútMinutes5 phút trước
1–23 giờHours3 giờ trước
1–6 ngàyDays2 ngày trước
7–27 ngàyWeeks1 tuần trước
28 ngày–11 thángMonths3 tháng trước
12+ thángYears1 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).

Cài đặt đơn vị và kiểu

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".

Các kiểu định dạng

  • .numeric — giá trị số đầy đủ: "3 ngày trước", "trong 2 tuần". Được khuyến nghị cho UI mặc định
  • .abbreviated — dạng rút gọn: "3 ng. trước", "trong 2 t.". Để hiển thị nhỏ gọn trong bảng và danh sách
  • .full — dạng chữ không có số: "ba ngày trước". Dành cho khả năng tiếp cận và giao diện giọng nói
  • .spellOut — dạng chữ cái với cách viết thay thế: "three days ago". Hiếm khi được sử dụng, chủ yếu cho các mục đích chuyên biệt

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.

swift
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.

RelativeDateTimeFormatter trong Swift: ví dụ

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).

swift
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.

swift
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.

Bản địa hóa ngày tương đối

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.

swift
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.

Lỗi định dạng phổ biến

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à gì?

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.

RelativeDateTimeFormatter chọn đơn vị như thế nào?

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".

Làm thế nào để thay đổi ngôn ngữ đầu ra?

Đặ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.

Sự khác biệt giữa .numeric và .abbreviated là gì?

.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.

Làm thế nào để hiển thị "vừa mới" thay vì "0 giây trước"?

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

  • RelativeDateTimeFormatter — một lớp tiện lợi để hiển thị ngày tương đối trong iOS 13+
  • Tự động bản địa hóa — biến cách chính xác cho tất cả các ngôn ngữ được hỗ trợ thông qua ICU
  • Ba kiểu — .numeric (tiêu chuẩn), .abbreviated (nhỏ gọn), .full (bằng chữ)
  • Chọn đơn vị — tự động dựa trên giá trị khác không lớn nhất
  • Cấu hình TimeZone — bắt buộc để đảm bảo nhất quán khi làm việc với ngày tháng từ máy chủ
  • Ngưỡng "vừa mới" — không được hỗ trợ tích hợp; yêu cầu kiểm tra thủ công khoảng thời gian
  • Không hỗ trợ "hôm qua" — trình định dạng không sử dụng dạng yesterday cho tiếng Anh

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.

Thảo luận dự án

Đọc thêm