RelativeDateTimeFormatter:本质,相对日期与 Swift

作者: IT Sectr 发布日期: 2026-07-13 阅读时间: 10 分钟

RelativeDateTimeFormatter 是 iOS 和 macOS 中的 Foundation 类,它将绝对日期转换为人类可读的相对表述:“5 分钟前”、“昨天”、“3 天后”。根据 Apple Developer Documentation, 2024RelativeDateTimeFormatter 会自动选择合适的单位(秒、分钟、小时、天),并以设备当前区域设置的语言本地化输出。与通过 Calendar 手动计算日期之间的差异不同,此类会考虑每种语言的语言特点:对于某些语言,数词会变形,对于其他语言,“昨天”一词会使用特殊形式。此类自 iOS 13 和 macOS 10.15 起可用。

要点

  • RelativeDateTimeFormatter — 用于在 iOS 和 macOS 中显示相对日期的类(iOS 13+)
  • 本地化输出 — 自动以当前区域设置的语言选择表述
  • 三种上下文类型 — past(之前)、future(之后)、present(现在),表述不同
  • 自动选择单位 — 秒、分钟、小时、天、周、月、年
  • 样式配置 — numeric(3 天后)或 abbreviated(3 天后)

什么是 RelativeDateTimeFormatter?

RelativeDateTimeFormatter 是 Foundation 中 Formatter 的子类,它接收 Date(或以秒为单位的差异)并返回带有相对时间的本地化字符串。例如,对于比当前日期早 5 分钟的日期,它将为 ru_RU 返回 “5 分钟前”,或为 en_US 返回 “5 minutes ago”。此类支持三种时间上下文:过去(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 如何显示“5 分钟前”?

工作算法 RelativeDateTimeFormatter 由三个步骤组成:计算传入日期与当前时刻之间的差异,选择合适的单位(不会产生零的最大单位),并考虑区域设置进行格式化。例如,对于 3720 秒(1 小时 2 分钟)的差异,将选择“小时”单位,结果是“1 小时前”,而不是“62 分钟前”。

单位按照“最大非零”原则选择:如果差异大于 86400 秒(1 天),则使用天;如果大于 604800(1 周)——使用周,依此类推。此算法保证结果始终自然可读:用户看到的不是“518400 秒前”,而是“6 天前”。单位的确切边界由当前区域设置的日历确定。

差异范围单位ru_RU 的示例
0–59 秒Seconds30 秒前
1–59 分钟Minutes5 分钟前
1–23 小时Hours3 小时前
1–6 天Days2 天前
7–27 天Weeks1 周前
28 天–11 个月Months3 个月前
12+ 个月Years1 年前

格式化上下文决定短语的结尾。对于过去:“前”(中文)、“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 分钟前”。

格式化样式

  • .numeric — 完整数值:“3 天前”、“2 周后”。默认推荐用于 UI
  • .abbreviated — 缩写形式:“3 天前”、“2 周后”。用于表格和列表中的紧凑显示
  • .full — 无数字的文字形式:“三天前”。用于无障碍和语音界面
  • .spellOut — 带替代拼写的字母形式:“three days ago”。很少使用,主要用于特殊用途

单位限制:默认情况下,RelativeDateTimeFormatter 只显示一个(最大的)单位。设置 maximumUnitCount = 2 会激活下一个单位以获得更精确的描述:“1 小时 30 分钟前”。然而,这可能会使短消息(推送、通知)的字符串过长。对于 UI,建议保持 maximumUnitCount = 1。

swift
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()))")

为不同上下文选择样式:对于新闻信息流,请使用 maximumUnitCount = 1 的 .numeric — 这是 Twitter、Instagram 和 Facebook 的标准。对于无障碍(VoiceOver),请使用 .full — 文字数字读起来更自然。对于紧凑元素(通知角标、状态栏),请使用 .abbreviated 以节省空间。

Swift 中的 RelativeDateTimeFormatter:示例

基本用法 RelativeDateTimeFormatter 归结为创建实例、配置属性和调用其中一种格式化方法。主要方法:localizedString(for:relativeTo:) — 用于一对日期,localizedString(fromTimeInterval:) — 用于以秒为单位的差异,以及 string(for:) — 用于带自动上下文(过去/未来)的 Date。

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

swift
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 规则覆盖所有这些情况,无需额外代码。

swift
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 在计算 .numeric 设置的差异时会忽略 TimeZone — 它使用以秒为单位的绝对差异。但是,对于 .full 样式(带文字数字)和特殊情况(昨天、今天),TimeZone 会被考虑在内。请始终显式设置 TimeZone 以保持一致性,尤其是当应用程序处理 UTC 中的服务器日期时。

格式化时的常见错误

计算相对日期时忽略 TimeZone — 处理服务器日期时的常见错误。如果服务器以 UTC 发送 Date,而 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?

RelativeDateTimeFormatter — 用于以相对格式显示日期的 Foundation 类:“5 分钟前”、“2 天后”。自 iOS 13 和 macOS 10.15 起可用。

RelativeDateTimeFormatter 如何选择单位?

根据最大非零单位原则 — 秒、分钟、小时、天、周、月或年。例如,对于 3720 秒(1 小时 2 分钟)的差异,将选择“小时”单位,而不是“分钟”。

如何更改输出语言?

将 locale 属性设置为所需的 Locale 实例。默认使用 Locale.current。示例:formatter.locale = Locale(identifier: "de_DE") 用于德语。

.numeric 和 .abbreviated 有什么区别?

.numeric — 完整形式(“3 天前”),.abbreviated — 缩写形式(“3 天前”)。选择取决于上下文:numeric 用于主要 UI,abbreviated 用于紧凑元素。

如何显示“刚刚”而不是“0 秒前”?

添加手动检查,用于小于 5–10 秒的间隔。RelativeDateTimeFormatter 不支持“刚刚”——对于小间隔,它返回“0 秒前”。使用带阈值的条件逻辑。

总结

  • RelativeDateTimeFormatter — 在 iOS 13+ 中显示相对日期的便捷类
  • 自动本地化 — 通过 ICU 为所有受支持语言正确变形
  • 三种样式 — .numeric(标准)、.abbreviated(紧凑)、.full(文字)
  • 单位选择 — 根据最大非零值原则自动选择
  • TimeZone 配置 — 处理服务器日期时保持一致所必需
  • “刚刚”阈值 — 不内置支持;需要手动检查间隔
  • 不支持“昨天” — 对于英语,formatter 不使用 yesterday 形式

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读