WWDC Quick Look 💓 By SwiftGGTeam
Formatters: Make data human-friendly

Formatters: Make data human-friendly

观看原视频

Highlight

iOS 14 改进 Formatter 对语言与地区组合的支持,并通过 DateFormatter、MeasurementFormatter、PersonNameComponentsFormatter、ListFormatter、NumberFormatter 和 stringsdict 帮助 App 生成符合用户习惯的日期、单位、姓名、列表、数字和复数字符串。

核心内容

很多 App 都在显示数据。天气 App 显示温度、风速和气压,健康 App 显示统计趋势,备忘录只是在标题下放一个修改时间。只要这些文本面向用户,就不能把 DateDouble 或数组直接拼成字符串。单位、日期顺序、小数点、百分号位置、姓名顺序都会随语言和地区改变。

这场 session 的入口案例很具体:一个住在阿布扎比、手机语言设为法语的人,过去可能看到更偏法国地区习惯的数字格式。iOS 14 和同代 macOS、watchOS、tvOS 改进了底层算法,让语言和地区组合得到更合适的格式。这个改动覆盖很多语言和地区组合,使用系统 Formatter 的 App 会随系统改进直接受益。

Karan Miśra 接着按“最具体到最通用”的顺序展示 Foundation 的格式化工具:日期和时间、度量单位、姓名、列表、数字,以及 SwiftUI 文本和 stringsdict。主线是少写自定义语言规则,把数据结构交给 Formatter,把复数规则交给 stringsdict,让系统按 locale 组装文本。

详细内容

日期和时间:用样式与模板,不要手写格式

02:25)Notes 顶部的最后修改时间,可以直接用 DateFormatter 的预设样式。Screen Time 这类界面需要“星期 + 日期 + 月份”或极短星期缩写时,再使用 setLocalizedDateFormatFromTemplate

// Date with Day/Month/Year and Time
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .short
dateFormatter.string(from: Date())

// Day of Week + Date + Month
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("MMMMdEEEE")
dateFormatter.string(from: Date())

// Abbreviated Day of Week
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("ccccc")
dateFormatter.string(from: Date())

关键点:

  • dateStyletimeStyle 适合常见组合,系统会补上符合 locale 的连接词和顺序。
  • setLocalizedDateFormatFromTemplate("MMMMdEEEE") 描述需要哪些字段,字段排列交给 Formatter。
  • ccccc 来自 Unicode 日期字段规则,适合独立显示的极短星期名。
  • transcript 明确提醒不要把 template 直接赋给 dateFormat,那会被字面解释,容易生成错误结果。

05:56)日期还包括持续时间、时间区间和相对时间。DateComponentsFormatterDateIntervalFormatterRelativeDateTimeFormatter 分别覆盖这些场景。

// Date and Time Components
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
let components = DateComponents(hour: 2, minute: 26)
formatter.string(from: components)

// Date and Time Intervals
let formatter = DateIntervalFormatter()
formatter.dateTemplate = "dMMM"
formatter.string(from: startDate, to: endDate)

// Relative Dates and Times
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
formatter.localizedString(from: DateComponents(day: -1))

关键点:

  • DateComponentsFormatter 适合“2 小时 26 分”这种时长。
  • DateIntervalFormatter 会避免重复起止日期里相同的元素。
  • RelativeDateTimeFormatter 适合过去或未来的自然语言表达,例如昨天这类命名式相对日期。

度量单位:保存一种单位,展示另一种地区习惯

06:29)Weather 示例里,数据源使用公制,展示给美国用户时会按地区习惯转换为华氏度、英里每小时等格式。开发者不需要自己维护单位换算和本地化字符串。

// Temperature
let formatter = MeasurementFormatter()
let temperature = Measurement<UnitTemperature>(value: 16, unit: .celsius)
formatter.numberFormatter.maximumFractionDigits = 0
formatter.string(from: temperature)

// Speed
let speed = Measurement<UnitSpeed>(value: 14, unit: .kilometersPerHour)
formatter.string(from: speed)

// Pressure
let pressure = Measurement<UnitPressure>(value: 1.01885, unit: .bars)
formatter.string(from: pressure)

关键点:

  • Measurement 保留值和原始单位,MeasurementFormatter 决定用户看到的单位和格式。
  • numberFormatter.maximumFractionDigits = 0 控制数字精度,单位本地化仍由系统处理。
  • transcript 提到 MeasurementFormatter 支持很多单位,也支持自定义单位。

姓名:姓名顺序和缩写都不能硬编码

07:49)姓名是非常个人化的数据。PersonNameComponentsFormatter 会结合用户语言设置、联系人设置和姓名本身选择顺序与样式。对于中日韩姓名,即使设备语言是英语,也应保持姓在名前。

let formatter = PersonNameComponentsFormatter()
var nameComponents = PersonNameComponents()
nameComponents.familyName = "Iwasaki"
nameComponents.givenName = "Akiya"
nameComponents.nickname = "Aki-chan"

// Full Name
formatter.string(from: nameComponents)

// Short Name: Respects User Preferences
formatter.style = .short
formatter.string(from: nameComponents)

// Abbreviated Name
formatter.style = .abbreviated
formatter.string(from: nameComponents)

关键点:

  • PersonNameComponents 让姓、名、昵称成为结构化数据。
  • .short 会尊重用户偏好,可能使用昵称或短名。
  • .abbreviated 适合头像缺失时的 monogram,但 session 提醒要检查长度和实际渲染尺寸。

08:31)monogram 不能保证所有名字都适合显示。Swift 的 count 按用户可见字符计数,可以先做一层长度检查。

formatter.style = .abbreviated
let monogram = formatter.string(from: nameComponents)
if monogram.count <= 2 {
    // Use Monogram
} else {
    // Use Icon
}

关键点:

  • count 不是 Unicode code point 数量,跨语言时更贴近用户看到的字符。
  • 字符数量只是一层保护,最终还要确认文本宽高能放进 UI。
  • 超出限制时应回退到通用图标,避免头像区域被长文本撑破。

列表和数字:语法细节交给系统

10:15)列表连接词会随语言和上下文改变。session 用键盘设置里的语言列表说明,西班牙语里的“and”会根据后一个词改变形式。iOS 14 同代系统更新了 ListFormatter,让它遵守多种语言的语法规则。

let items = ["English", "French", "Spanish"]
ListFormatter.localizedString(byJoining: items)

let items = ["English", "Spanish"]
ListFormatter.localizedString(byJoining: items)

let items = ["Spanish", "English"]
ListFormatter.localizedString(byJoining: items)

关键点:

  • ListFormatter.localizedString(byJoining:) 接收数组,返回用户语言下的自然列表。
  • 数组顺序改变时,连接词也可能改变。
  • 使用系统 API 后,底层实现更新会让 App 获得新的语言规则。

12:01)数字的风险更隐蔽。32.768 在不同地区可能被理解为三万多或三十二点七六八。NumberFormatter 会处理小数分隔符、数字形状、百分号位置等细节。

let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.string(from: 32.768) // French (France)

let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.string(from: 32.768) // Arabic (Egypt)

formatter.percentSymbol
formatter.decimalSeparator

关键点:

  • .decimal 让普通数字按语言和地区输出。
  • percentSymboldecimalSeparator 这类属性适合必须读取符号本身的场景。
  • session 还展示了 .percent,土耳其语环境会把百分号放在数字前面。

SwiftUI 文本:复数规则放进 stringsdict

13:24)Formatter 覆盖常见数据类型,业务自己的句子仍然需要本地化。SwiftUI 里可以像平时一样写 Text,再用 stringsdict 提供对应 key 的复数规则。

var body: some View {
    Text("\(photosCount) Photos Selected")
}

关键点:

  • photosCount 进入本地化字符串后,stringsdict 决定单数、双数、少数、其他等分支。
  • transcript 用英语和阿拉伯语对比:1、2、3 到 10、11 以上会触发不同语法形式。
  • 代码只写一次,新增语言时主要补翻译和复数规则。

核心启发

  • 多地区天气卡片:做什么:把温度、风速、气压统一建模为 Measurement。为什么值得做:数据源可以继续用公制,界面按用户地区显示合适单位。怎么开始:先把展示层的手写单位字符串替换为 MeasurementFormatter

  • 健康或运动时长摘要:做什么:把“本周训练 2 小时 26 分”“昨天完成训练”这类文案交给日期 Formatter。为什么值得做:持续时间、区间和相对时间在不同语言里有不同顺序和词形。怎么开始:时长用 DateComponentsFormatter,日期范围用 DateIntervalFormatter,相对日期用 RelativeDateTimeFormatter

  • 联系人头像兜底:做什么:没有头像时展示姓名缩写,过长时回退图标。为什么值得做:姓名顺序、昵称、缩写规则和中日韩姓名显示都不适合硬编码。怎么开始:用 PersonNameComponentsFormatter 生成 .abbreviated 样式,再用字符数和实际布局尺寸做保护。

  • 筛选条件自然语言摘要:做什么:把“英语、西班牙语和法语”这类列表显示在设置页或搜索结果页。为什么值得做:列表连接词会随语言和元素顺序变化,手写连接符容易出错。怎么开始:保留数组结构,到展示层调用 ListFormatter.localizedString(byJoining:)

  • 照片选择复数文案:做什么:让“1 Photo Selected”“2 Photos Selected”以及阿拉伯语双数、少数等形式走本地化资源。为什么值得做:复数规则不是简单的单复数切换。怎么开始:SwiftUI 继续使用 Text("\(photosCount) Photos Selected"),为对应 key 配置 stringsdict。

关联 Session

评论

GitHub Issues · utterances