DateFormatter:核心概念、日期格式化与本地化

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

DateFormatter — 这是一个 Foundation 框架中的类,用于在 Date 对象与其字符串表示形式之间进行双向转换。该类会考虑用户的区域设置、时区和日历,确保在世界任何地区都能正确显示日期。根据 Apple Developer Documentation (2025),DateFormatter 支持四种预设的日期和时间样式,以及通过模板字符串实现完全自定义的格式。在面向国际化的应用中,没有 DateFormatter 就无法向用户正确显示日期。

要点

  • DateFormatter — 用于将 Date 转换为字符串及反向转换的类,考虑区域设置和时区。
  • dateStyle 和 timeStyle — 预设样式(.short、.medium、.long、.full),用于快速格式化。
  • dateFormat — 用于自定义格式的模板字符串,通过 Unicode LDML 规范中的字符来定义。
  • LocaleTimeZone — 格式化器的属性,决定区域显示和时区。
  • ISO8601DateFormatter — 在 API 序列化中处理 ISO 8601 格式的更快替代方案。

什么是 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 中,格式化器通常缓存在静态属性中,或在首次访问时惰性创建。

iOS SDK 中的 DateFormatter

DateFormatter 被应用于 iOS 的许多系统组件中。UIDatePicker 内部使用 DateFormatter 以 countDownTimer 模式显示日期。带有格式化器的 TextField 可以在输入时自动验证用户输入的日期。Core Data 支持 Date 类型的属性,但其字符串显示始终通过 DateFormatter 完成。

线程安全性 — DateFormatter 不是线程安全的。从不同线程修改格式化器的属性会导致未定义的行为。对于多线程使用,请为每个线程创建独立的格式化器实例,或通过 NSLockserial queue 进行同步。

DateFormatter 的格式化样式

dateStyletimeStyle 是配置日期显示的最简单方式。每种样式都有四个变体:.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)
.short21.07.20267/21/26
.medium2026年7月21日Jul 21, 2026
.long2026年7月21日July 21, 2026
.full2026年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 自定义格式

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”) 以获得固定的跨区域格式,尤其是在解析服务器响应的日期时。

swift
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 字符在不同区域设置中的工作方式不同。

本地化与 TimeZone

Locale 决定了月份、星期几名称的显示方式以及使用哪些分隔符。DateFormatter 默认使用 Locale.current,但在某些场景下需要指定特定的区域设置:对于日志中的固定格式,请使用 en_US_POSIX;对于服务器日期,请使用与服务器对应的区域设置。

TimeZone 属性决定显示的时区。默认使用系统时区,但对于拥有国际用户的应用,通常需要以用户的时区或 UTC 显示日期。更改 timeZone 只影响显示 — Date 的值保持不变。

一个重要特性:如果 DateFormatter 用于解析字符串,而字符串中包含时区指示(例如,带 Z 表示 UTC 的 “2026-07-21T14:30:00Z”),则 timeZone 属性会被忽略 — 格式化器会使用字符串中的时区。如果字符串中没有时区,则使用格式化器的 timeZone。

swift
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 与替代方案

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 端点必须保持一致。

swift
// 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 小时的阈值,对于即时通讯工具使用一周。

swift
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 时区。这种方法可以确保文件在任何国家都能正确打开,不受系统区域设置的影响。

场景格式化器关键配置
新闻流RelativeDateFormatterunitsStyle = .full
日期输入DateFormatterdateFormat + fallback
API 序列化ISO8601DateFormatterwithInternetDateTime
报告导出DateFormatteren_US_POSIX + UTC

常见问题

为什么 DateFormatter 对正确的字符串返回 nil?

最常见的原因是 dateFormat 与字符串格式不匹配。例如,格式 “dd.MM.yyyy” 无法解析字符串 “2026-07-21”。第二个原因是区域设置不匹配:字符串 “July 21, 2026” 无法使用 ru_RU 区域设置解析。第三个原因是 LDML 字符中的拼写错误:请使用 yyyy,而不是 YYYY(含义不同)。

每次调用都需要创建新的 DateFormatter 吗?

不需要。DateFormatter 是一个较重的对象,其初始化包括加载区域设置数据。请为每种格式化类型创建一个实例并重复使用。在多线程环境中,请使用线程本地存储(thread-local storage)或带 serial queue 的格式化器池进行同步。

DateFormatter 与 RelativeDateFormatter 有什么区别?

DateFormatter 显示绝对日期(2026年7月21日),而 RelativeDateFormatter 显示相对日期(今天、昨天、3天后)。RelativeDateFormatter 出现在 iOS 15+ 中,使用相同的 LDML 模板,但会自动选择相对显示方式。

如何处理来自 API 的没有时区的日期?

在解析前将格式化器的 timeZone 设置为 UTC。如果服务器返回的是没有时区指示的本地时间日期,请确认 API 规范 — 很可能指的是 UTC。对于以 Z 结尾的 ISO 8601 格式,不需要 timeZone — 格式化器会从字符串中解析偏移量。

如何让 DateFormatter 成为线程安全的?

不要在没有同步的情况下从不同线程使用同一个实例。请在每个线程中创建新实例,或使用 Thread.current.threadDictionary 进行存储。另一种方案是使用 NSLock,在 string(from:) 和 date(from:) 执行期间加锁。

总结

  • DateFormatter — 用于将 Date 转换为字符串及反向转换的 Foundation 类,考虑用户的区域设置、时区和日历。
  • 预设样式 dateStyle 和 timeStyle,具有 .short、.medium、.long、.full 变体,可覆盖大多数 UI 场景。
  • 自定义 dateFormat 通过 LDML 字符提供对格式的完全控制,但需要谨慎处理本地化。
  • Locale 和 TimeZone — 正确显示所必需的属性:对于 UI 使用系统区域设置,对于 API 使用 en_US_POSIX 和 UTC。
  • ISO8601DateFormatter — 由于速度快和稳定性好,是 API 中日期序列化的首选。
  • DateFormatter 不是线程安全的 — 为每个线程使用独立实例,或同步访问。
  • RelativeDateFormatter(iOS 15+)— 在新闻流和即时通讯工具中显示相对日期的最佳解决方案。

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

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

讨论项目

另请阅读