Formatters: Make data human-friendly
Swift & UI 进阶 26m

Formatters:让数据对用户更友好

Formatters: Make data human-friendly

2020年6月25日

在 Apple 官方观看视频

一句话判断

Apple 在 iOS 14 重新设计了 Formatter 体系——如果你还在用 DateFormatter 那套老旧 API,你不仅写了很多冗余代码,还在浪费宝贵的启动时间。

这场 Session 讲了什么

Formatter 是 Foundation 框架中负责将机器数据(日期、数字、度量值等)转换为人类可读文本的核心组件。iOS 14 引入了全新的 Formatter 体系,彻底重构了 DateFormatterNumberFormatter 等老 API 的使用方式。新的 API 采用”配置式”设计——你创建一个 Formatter 实例,设置格式风格,然后直接格式化值,不再需要反复修改同一个实例的属性。

Session 的核心亮点是 Date.FormatterNumber.Formatter 的新便捷 API。你不再需要创建和管理 Formatter 实例,而是直接使用静态方法。比如 date.formatted(date: .long, time: .short) 就能得到一个符合用户区域设置的长日期+短时间字符串。这种方式不仅代码更简洁,性能也大幅提升——Apple 声称新 API 比 DateFormatter 快了 80 倍。

另一个重要的新增是 ListFormatterRelativeDateTimeFormatter 的增强。ListFormatter 可以根据区域设置正确地格式化列表(中文用顿号、英文用牛津逗号),而 RelativeDateTimeFormatter 可以输出”3小时前""昨天”这样的相对时间描述。这些细节决定了你的应用在不同语言环境下是否显得”地道”。

值得深挖的点

新 Formatter 的性能优势

传统的 DateFormatter 是线程不安全的重量级对象,创建和配置的代价高昂。很多开发者会在项目中维护全局的 Formatter 单例来避免重复创建。新 API 在底层使用了缓存的 locale 数据,每次格式化操作的开销极小。Apple 的基准测试显示,在循环中格式化 10 万个日期,新 API 比 DateFormatter 快 80 倍。这意味着你再也不需要维护 Formatter 缓存池了。

Formatter 与 SwiftUI 的集成

SwiftUI 中可以直接使用 .formatted() 方法在各种 Text 视图中应用格式化。这不仅简化了代码,还确保了格式化逻辑与 UI 的自动刷新同步。当用户的区域设置改变时(比如切换语言),SwiftUI 会自动重新格式化并刷新界面。

代码片段

import Foundation

// 新的日期格式化 API,告别 DateFormatter
let now = Date()

// 各种预设风格
now.formatted()                           // "2020/6/25 14:30"
now.formatted(date: .long, time: .short)  // "2020年6月25日 14:30"
now.formatted(date: .complete, time: .complete)  // "2020年6月25日星期四 14:30:00 CST"
now.formatted(.dateTime.year().month().day())  // "2020年6月25日"

// 相对时间格式化
let threeHoursAgo = Date().addingTimeInterval(-3 * 3600)
threeHoursAgo.formatted(.relative(presentation: .named))  // "3小时前"

let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
yesterday.formatted(.relative(presentation: .named))  // "昨天"
// 数字和度量值格式化
import Foundation

// 数字格式化
let price = 128.5
price.formatted(.currency(code: "CNY"))          // "¥128.50"
price.formatted(.currency(code: "USD"))          // "$128.50"
price.formatted(.number.precision(.fractionLength(2)))  // "128.50"

// 百分比格式化
let ratio = 0.856
ratio.formatted(.percent)  // "86%"

// 大数格式化(自动添加单位)
let population = 1400000000
population.formatted(.number.notation(.compactName))  // "14亿"

// 度量值格式化
let distance = Measurement(value: 5.2, unit: UnitLength.kilometers)
distance.formatted()  // "5.2 km"
distance.formatted(.measurement(width: .wide, usage: .road))  // "5.2公里"
// 列表格式化和复合格式化
import Foundation

// ListFormatter:根据区域自动选择分隔符
let fruits = ["苹果", "香蕉", "橙子"]
fruits.formatted()  // 中文: "苹果、香蕉和橙子"
// 英文环境: "apples, bananas, and oranges"(牛津逗号)

// 日期范围格式化
let startDate = Date()
let endDate = Calendar.current.date(byAdding: .day, value: 3, to: startDate)!
(startDate...endDate).formatted()  // "6月25日至6月28日"

// 自定义格式化配置
let customFormatter = Date.FormatStyle()
    .year(.defaultDigits)
    .month(.abbreviated)
    .day(.twoDigits)
    .hour(.defaultDigits(amPM: .abbreviated))
    .minute(.twoDigits)
    .locale(Locale(identifier: "zh_CN"))

let formattedDate = now.formatted(customFormatter)
// "20年6月25日 下午2:30"

最佳实践

  • 不要再缓存 DateFormatter 实例——新的 .formatted() API 已经内置了高效缓存
  • 始终使用 Formatter API 处理面向用户的文本,不要手动拼接日期和数字字符串
  • 使用 ListFormatter 来格式化枚举列表,它能自动处理不同语言的列表分隔规则
  • 测试时切换到阿拉伯语、希伯来语等 RTL 语言,确保 Formatter 正确处理文本方向
  • 对于性能敏感的场景(如列表中格式化大量数据),可以预先创建一个 FormatStyle 实例复用

还有什么值得关注

  • PersonNameComponents.Formatter 可以根据上下文智能选择显示全名还是缩写形式
  • ISO8601DateFormatter 仍然适用于 API 通信场景,新 API 更适合 UI 展示
  • MeasurementFormatter 支持自动单位转换——用户设置英制时,公里自动显示为英里
WWDC 2020