DateFormatter — 这是一个 Foundation 框架中的类,用于在 Date 对象与其字符串表示形式之间进行双向转换。该类会考虑用户的区域设置、时区和日历,确保在世界任何地区都能正确显示日期。根据 Apple Developer Documentation (2025),DateFormatter 支持四种预设的日期和时间样式,以及通过模板字符串实现完全自定义的格式。在面向国际化的应用中,没有 DateFormatter 就无法向用户正确显示日期。
要点
DateFormatter — 这是来自 Foundation 框架的一个类,实现了 Date 与字符串之间的双向转换。它以 NSDateFormatter 的形式出现在 OpenStep 中,并且至今仍是所有 Apple 平台上主要的日期格式化工具。该类继承自 Formatter,并提供便捷的 API 来实现本地化的日期显示。
DateFormatter 的工作原理基于 Unicode LDML 模板 — 与 ICU(International Components for Unicode)使用的模板相同。模板通过 dateFormat 属性定义,其中 y、M、d、H、m、s 分别对应年、月、日、时、分、秒。字符的重复决定格式:“y” — 两位数的年份,“yyyy” — 四位数。
创建 DateFormatter 是一个开销较大的操作,因为初始化时会加载区域设置和日历数据。Apple 建议为每种格式化类型创建一次格式化器并重复使用。在 SwiftUI 和 UIKit 中,格式化器通常缓存在静态属性中,或在首次访问时惰性创建。
DateFormatter 被应用于 iOS 的许多系统组件中。UIDatePicker 内部使用 DateFormatter 以 countDownTimer 模式显示日期。带有格式化器的 TextField 可以在输入时自动验证用户输入的日期。Core Data 支持 Date 类型的属性,但其字符串显示始终通过 DateFormatter 完成。
线程安全性 — DateFormatter 不是线程安全的。从不同线程修改格式化器的属性会导致未定义的行为。对于多线程使用,请为每个线程创建独立的格式化器实例,或通过 NSLock 或 serial queue 进行同步。
dateStyle 和 timeStyle 是配置日期显示的最简单方式。每种样式都有四个变体:.short、.medium、.long、.full。组合 dateStyle 和 timeStyle 可以独立配置日期和时间的格式,而 .none 属性则关闭相应的部分。
对于美国区域设置,.short 将日期格式化为 “7/21/26”,而对于俄语区域设置则格式化为 “21.07.2026”。.long 样式对于俄语区域设置显示 “2026年7月21日”,而 .full 显示 “2026年7月21日星期二” 并注明星期几。所有四种样式都会自动适应当地标准,包括组件顺序和分隔符。
SFDateFormatter 在 iOS 15+ 中提供了通过 RelativeDateFormatter 和 DateIntervalFormatter 实现的替代方法。RelativeDateFormatter 显示 “今天”、“昨天”、“3天后”,用于即时上下文。DateIntervalFormatter 显示日期范围:“2026年7月21日–25日” — 用于预订和计划。
| 样式 | 示例(ru_RU) | 示例(en_US) |
|---|---|---|
| .short | 21.07.2026 | 7/21/26 |
| .medium | 2026年7月21日 | Jul 21, 2026 |
| .long | 2026年7月21日 | July 21, 2026 |
| .full | 2026年7月21日星期二 | Tuesday, July 21, 2026 |
组合样式时,DateFormatter 会自动选择分隔符:对于 .short.date + .short.time,结果可能是 “21.07.2026, 14:30”。对于 .full.date + .full.time — “2026年7月21日星期二, 14:30:00 MSK”。分隔符由区域设置决定,而非开发者 — 这确保了符合用户的区域预期。
dateFormat 允许使用 Unicode LDML 规范的字符来定义任意的格式化模板。这提供了对显示方式的完全控制:可以只显示年份和月份,或者只显示星期几而不显示日期,或者显示不含秒的时间。对于特定的设计需求,自定义格式是不可或缺的。
主要字符是 yyyy(年:2026)、MM(月:07)、dd(日:21)、HH(时:14)、mm(分:30)、ss(秒:00)。要显示月份的完整名称,请使用 MMMM(7月),缩写则使用 MMM(7月)。星期几为 EEEE(星期二),缩写为 E(周二)。
使用 dateFormat 时,设置格式化器的 locale 非常重要。如果未设置 locale,格式化器将使用系统区域设置,这对于 API 中的固定格式可能是不希望的。Apple 建议设置 locale = Locale(identifier: “en_US_POSIX”) 以获得固定的跨区域格式,尤其是在解析服务器响应的日期时。
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ru_RU")
formatter.dateFormat = "d MMMM yyyy"
let customString = formatter.string(from: Date())
// "2026年7月21日"
// 解析自定义字符串
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let date = formatter.date(from: "2026-07-21 14:30:00")!
dateFormat 中的错误是应用崩溃的常见原因之一。如果格式与字符串不匹配,date(from:) 方法会返回 nil。使用 guard let 或 ?? 安全地解包可选值。要在所有支持的语言上测试格式 — 某些 LDML 字符在不同区域设置中的工作方式不同。
Locale 决定了月份、星期几名称的显示方式以及使用哪些分隔符。DateFormatter 默认使用 Locale.current,但在某些场景下需要指定特定的区域设置:对于日志中的固定格式,请使用 en_US_POSIX;对于服务器日期,请使用与服务器对应的区域设置。
TimeZone 属性决定显示的时区。默认使用系统时区,但对于拥有国际用户的应用,通常需要以用户的时区或 UTC 显示日期。更改 timeZone 只影响显示 — Date 的值保持不变。
一个重要特性:如果 DateFormatter 用于解析字符串,而字符串中包含时区指示(例如,带 Z 表示 UTC 的 “2026-07-21T14:30:00Z”),则 timeZone 属性会被忽略 — 格式化器会使用字符串中的时区。如果字符串中没有时区,则使用格式化器的 timeZone。
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ru_RU")
formatter.timeZone = TimeZone(identifier: "Europe/Moscow")
formatter.dateStyle = .long
formatter.timeStyle = .short
let moscowTime = formatter.string(from: Date())
// "2026年7月21日, 14:30"
// 解析字符串中不含时区的时间
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd HH:mm"
let utcDate = formatter.date(from: "2026-07-21 10:30")!
AutoupdatingCurrentLocale — 一种特殊的区域设置类型,当用户的系统设置发生变化时会自动更新。DateFormatter 默认支持它。如果应用在后台运行,而用户更改了系统语言,则在更改之前创建的格式化器将继续使用旧的区域设置 — 要更新,需要创建新实例。
ISO8601DateFormatter — 用于处理 ISO 8601 格式日期的专用格式化器。该格式是 REST API、JSON 和数据交换的事实标准。ISO8601DateFormatter 比 DateFormatter 快得多,因为它不依赖区域设置,并使用固定的解析语法。
格式化器的主要选项是 .withInternetDateTime(2026-07-21T14:30:00Z)、.withFractionalSeconds(添加毫秒)、.withTimeZone(包含时区偏移)。组合这些选项可以获得任何 ISO 8601 变体:带毫秒、带时区、只有日期不含时间。
JSONEncoder.DateEncodingStrategy 允许为所有 Codable 模型全局配置日期的编码方式。选项包括 .iso8601(使用 ISO8601DateFormatter)、.formatted(DateFormatter)、.millisecondsSince1970、.secondsSince1970。策略的选择会影响序列化的整个生命周期,并且所有 API 端点必须保持一致。
// ISO8601DateFormatter
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let isoString = isoFormatter.string(from: Date())
// "2026-07-21T14:30:00.000Z"
// 使用 ISO8601 的 JSONEncoder
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
// 替代方案:使用自定义格式化器的 JSONEncoder
let customEncoder = JSONEncoder()
customEncoder.dateEncodingStrategy = .formatted(myFormatter)
DateFormatter 与 ISO8601DateFormatter 对比 — 对于 API 中日期的序列化和解析,请选择 ISO8601DateFormatter,因为它比 DateFormatter 快 5-10 倍,且不易出现本地化错误。DateFormatter 则用于用户界面,在界面中需要使用用户母语显示月份和星期几的本地化名称。
让我们来看一下 DateFormatter 在 iOS 应用中的真实使用场景:新闻列表中的显示、出生日期输入,以及包含不同时区日期的报告导出。
RelativeDateFormatter 最适合新闻流。它会为新新闻显示 “刚刚”、“5分钟前”、“昨天”,并为旧新闻切换到完整日期。切换阈值通过 calendar 配置:对于新闻使用 24 小时的阈值,对于即时通讯工具使用一周。
func formatRelativeDate(_ date: Date) -> String {
let relative = RelativeDateFormatter()
relative.unitsStyle = .full
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
let daysDiff = Calendar.current.dateComponents(
[.day], from: date, to: Date()
).day ?? 0
return daysDiff < 1
? relative.localizedString(for: date, relativeTo: Date())
: formatter.string(from: date)
}
出生日期输入 — 另一个常见场景。DateFormatter 使用特定的 dateFormat “dd.MM.yyyy” 和 locale “ru_RU” 进行配置。在解析输入的字符串时,处理可能的错误非常重要:格式化器对无效字符串返回 nil。成功解析后,需要检查日期是否在允许的范围内 — 不早于 1900 年,不晚于今天。
导出包含日期的报告需要不依赖用户区域设置的固定格式。请使用 dateFormat “yyyy-MM-dd HH:mm:ss” 搭配 locale en_US_POSIX 和 UTC 时区。这种方法可以确保文件在任何国家都能正确打开,不受系统区域设置的影响。
| 场景 | 格式化器 | 关键配置 |
|---|---|---|
| 新闻流 | RelativeDateFormatter | unitsStyle = .full |
| 日期输入 | DateFormatter | dateFormat + fallback |
| API 序列化 | ISO8601DateFormatter | withInternetDateTime |
| 报告导出 | DateFormatter | en_US_POSIX + UTC |
常见问题
最常见的原因是 dateFormat 与字符串格式不匹配。例如,格式 “dd.MM.yyyy” 无法解析字符串 “2026-07-21”。第二个原因是区域设置不匹配:字符串 “July 21, 2026” 无法使用 ru_RU 区域设置解析。第三个原因是 LDML 字符中的拼写错误:请使用 yyyy,而不是 YYYY(含义不同)。
不需要。DateFormatter 是一个较重的对象,其初始化包括加载区域设置数据。请为每种格式化类型创建一个实例并重复使用。在多线程环境中,请使用线程本地存储(thread-local storage)或带 serial queue 的格式化器池进行同步。
DateFormatter 显示绝对日期(2026年7月21日),而 RelativeDateFormatter 显示相对日期(今天、昨天、3天后)。RelativeDateFormatter 出现在 iOS 15+ 中,使用相同的 LDML 模板,但会自动选择相对显示方式。
在解析前将格式化器的 timeZone 设置为 UTC。如果服务器返回的是没有时区指示的本地时间日期,请确认 API 规范 — 很可能指的是 UTC。对于以 Z 结尾的 ISO 8601 格式,不需要 timeZone — 格式化器会从字符串中解析偏移量。
不要在没有同步的情况下从不同线程使用同一个实例。请在每个线程中创建新实例,或使用 Thread.current.threadDictionary 进行存储。另一种方案是使用 NSLock,在 string(from:) 和 date(from:) 执行期间加锁。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。