在日常生活里,人们经常需要在各种度量衡单位之间转换。对开发者而言,实现这类功能看似简单:写几行公式、做几次 switch
,似乎就能完成任务。但当你希望同时支持数十种单位、无缝国际化,并且还要考虑格式化、精度、舍入等细节时,工作量立刻飙升,且枯燥到足以让人怀疑人生。好消息是——从 iOS 10 起,苹果在 Foundation 中加入了完善的 Measurement API,帮我们把这一切“苦力活”都完成了。本文将带你系统地了解它的用法与实践。
初识 Measurement
Measurement
是一个用来表示“带单位数值”的结构体,内部封装了:
value: Double // 数值
unit : Unit // 单位
Measurement
提供了类型安全的单位换算能力。一旦实例化,它便是不可变的(遵循 struct
的值类型语义)。
// 只有数值
let height: Double = 180
// 同时包含数值与单位
let heightMeasurement = Measurement(value: 180, unit: UnitLength.centimeters)
// 也可使用泛型写法明确单位类型
let heightMeasurement = Measurement<UnitLength>(value: 180, unit: UnitLength.centimeters)
相比仅用 Double
来存储数值,Measurement
不仅在语义上更加清晰,还内置了便捷的转换功能:
// 直接转换为米
let meters = heightMeasurement.converted(to: .meters).value
print(meters) // 1.8
目前,Foundation 为 Measurement 内置了涵盖长度、质量、持续时间、加速度、面积、温度等数十种 单位类别,足以覆盖绝大多数日常开发场景。
数学运算
Measurement
的一个便利之处在于,只要它们属于同一单位类别(UnitType),你就可以直接对它们进行数学运算,例如加法、减法、比较,甚至可以用它们来定义数值区间。
let heightMeasurement = Measurement<UnitLength>(value: 180, unit: .centimeters)
// 除法 (与标量运算)
let half = heightMeasurement / 2
print(half.value) // 90.0
print(half) // "90.0 cm"
// 加法:不同单位亦可相加,结果会自动转换为该类别的基准单位
let h180cm = Measurement<UnitLength>(value: 180, unit: .centimeters)
let h1m = Measurement<UnitLength>(value: 1, unit: .meters)
let totalHeight = h180cm + h1m // 结果为 2.8 m (因为 UnitLength 的基准单位是米)
print(totalHeight) // "2.8 m"
// 比较:自动处理单位转换
h180cm < h1m // false (180 cm 不小于 1 m)
// 区间:同样支持,单位会被考虑在内
let range = h1m ... h180cm // 区间从 1 米 到 1.8 米
range.contains(Measurement(value: 6, unit: .feet)) // false (约 1.83 米,不在区间内)
这种设计无需手动进行单位换算,并且运算过程中单位信息始终得以保留,从而显著提升了代码的可读性与安全性。
提示:当对两个属于同一类别的
Measurement
实例执行+
或-
运算时,结果会自动转换为该 UnitType 的基准单位(base unit)。我们将在稍后详细讨论基准单位的概念。
格式化
将 Measurement
实例转换为用户友好的、本地化的文本是常见需求。Measurement
提供了强大的 formatted()
方法来实现这一点。
最简单的用法是直接调用 formatted()
,它会根据当前的 Locale 设置自动选择合适的单位和格式:
let h180cm = Measurement<UnitLength>(value: 180, unit: UnitLength.centimeters)
h180cm.formatted()
这段代码的输出会因用户的区域设置而异:
- 在美国 (en-US) → 5.9 ft (自动转换为英尺)
- 在中国 (zh-CN) → 1.8 m (自动转换为米)
当你需要更精细地控制输出格式时,可以为 formatted()
提供一个 MeasurementFormatStyle
:
let str = h180cm.formatted(
.measurement(
width: .wide, // 单位长度:wide / abbreviated / narrow
usage: .asProvided, // 场景:.asProvided / .person / .road / …
numberFormatStyle: .number
.precision(.fractionLength(2)) // 数值格式
)
)
print(str) // "180.00 centimeters"
参数 | 说明 |
---|---|
width | 指定单位文本的详细程度。wide → “centimeters”,abbreviated → “ cm”,narrow → “cm” 。 |
usage | 提示格式化器该数值的使用场景,以便系统能自动选择最恰当的单位并进行智能舍入。例如,.personHeight 在英制地区可能会将 180 cm 显示为 “5 ft 11 in”,而 .road 则可能将较小的单位聚合成更常用的道路距离单位 |
numberFormatStyle | 用于精细控制数值部分的显示方式,它本身就是一个强大的 FormatStyle ,可以设置小数位数、分组、科学计数法等。 |
通过这些选项,格式化过程不仅会根据 Locale
和 usage
智能地转换单位,还会翻译单位名称(例如 centimeters
vs 厘米
),从而提供真正贴合用户区域与习惯的展示。
在 SwiftUI 中优雅展示
利用 Text
视图接受 FormatStyle
的初始化器,你可以轻松地将 Measurement
格式化并显示出来,并且它会自动响应环境中的 Locale
设置:
struct MeasurementDisplay: View {
let h180cm = Measurement<UnitLength>(value: 180, unit: UnitLength.centimeters)
var body: some View {
VStack(alignment: .leading) {
// 示例 1: 在美国英语环境下,使用 .person 场景,缩写单位
Text(h180cm, format: .measurement(width: .abbreviated, usage: .person))
.environment(\.locale, .init(identifier: "en_US"))
// 换成 .personHeight 显示 5 ft, 11 in
// 示例 3: 在中文环境下,使用 .person 场景,缩写单位
Text(h180cm, format: .measurement(width: .abbreviated, usage: .personHeight)) // 或者 .person
.environment(\.locale, .init(identifier: "zh_CN"))
// 示例 4: 在法语环境下,使用 .road 场景,完整单位名
Text(h180cm, format: .measurement(width: .wide, usage: .road))
.environment(\.locale, .init(identifier: "fr_FR"))
// 输出: "2 mètres" (针对 road 场景,1.8m 被智能舍入为更常用的 2m)
}
}
}
如果你需要更精细的控制,比如为数值和单位应用不同的样式(颜色、字体、粗细等),你可以先将 Measurement
格式化为 AttributedString
,然后修改其特定部分的属性:
struct AttributedMeasurementView: View {
let weight = Measurement(value: 100, unit: UnitMass.kilograms) // 100 kg
var styledWeightAttributedString: AttributedString {
// 使用 .attributed 将 Measurement 格式化为 AttributedString
var str = weight.formatted(
.measurement(
width: .abbreviated,
usage: .asProvided,
numberFormatStyle: .number)
.attributed
)
// 精准控制 value 和 unit 的样式
let _ = str.transformingAttributes(\.measurement) { info in
switch info.value {
case .value:
str[info.range].font = .title
case .unit:
str[info.range].foregroundColor = .orange
str[info.range].font = .italic(.body)()
default:
break
}
}
return str
}
var body: some View {
Text(styledWeightAttributedString)
.foregroundStyle(.blue.gradient)
}
}
若想更全面地了解 AttributedString
的强大功能及其在 SwiftUI 中的应用,推荐阅读:
新瓶装旧酒:与 NSMeasurement 的桥接
如果你用过 NSMeasurement
,可能会疑惑:Measurement
是全新的 Swift 实现吗?其实它们本质上是同一套底层逻辑的不同包装:
public struct Measurement<UnitType>: ReferenceConvertible, Comparable, Equatable
where UnitType: Unit { … }
通过 ReferenceConvertible
,Measurement
(值类型)可与 NSMeasurement
(引用类型)互转。
let h1m = Measurement(value: 1, unit: UnitLength.meters)
let ns = h1m as NSMeasurement // Swift → Obj-C
print(ns.doubleValue) // 1
let nsCM = NSMeasurement(doubleValue: 100, unit: UnitLength.centimeters)
let cm = nsCM as Measurement // Obj-C → Swift
print(cm.unit) // cm
尽管 Swift Foundation 正在进行纯 Swift 重构,但 Measurement 的重写工作尚未完成。这意味着其功能当前仅能在苹果平台上有效运行。为了代码兼容性,非苹果平台提供了 Stub 实现,确保包含 Measurement 的代码可以编译成功。然而,请务必注意:这些 Stub 不具备实际的单位转换和格式化功能。我们期待未来能迎来真正跨平台的 Measurement 支持。
在现有类别中新增单位
有时,我们需要的单位属于一个已有的类别(比如质量 UnitMass
、长度 UnitLength
),但系统并未内置该特定单位。我们可以通过 Swift 的 extension
机制,方便地为这些现有类别补充我们需要的单位。
以下是如何为 UnitMass 添加中国大陆常用的“市斤”(等于 0.5 千克)的示例:
extension UnitMass {
/// 添加中国特有的质量单位:市斤 (jin)
/// 1 市斤 = 0.5 千克 (kilograms)
static var jin: UnitMass {
// 1. 定义单位符号 (Symbol)
// 使用 NSLocalizedString 是一种好习惯,便于未来对符号本身进行基础本地化
let symbol = NSLocalizedString("斤", comment: "Symbol for the Chinese unit of mass 'jin'")
// 2. 定义转换器 (Converter)
// 指明该单位与此类别的基准单位 (Base Unit) 的换算关系
// UnitMass 的基准单位是 kilograms (千克)
// coefficient: 0.5 表示 1 jin = 0.5 kilograms
let converter = UnitConverterLinear(coefficient: 0.5)
// 3. 使用符号和转换器创建并返回新的 UnitMass 实例
return UnitMass(symbol: symbol, converter: converter)
}
}
let weight = Measurement(value: 1, unit: UnitMass.kilograms)
print(weight.converted(to: .jin).value) // 2.0
symbol
: 定义单位的文本表示符号。- 推荐使用
NSLocalizedString
,这允许你为不同语言提供该符号的基础翻译(例如,如果需要在英文环境下显示 “Jin” 而不是 “斤”)。 - 局限性: 请注意,通过这种方式添加的自定义单位符号,目前无法像系统内置单位那样,根据
formatted()
方法中的width
参数(如.wide
,.abbreviated
,.narrow
)自动展示不同长度的本地化形式。它通常只会显示你提供的这个symbol
。
- 推荐使用
converter
: 这是核心部分,用于定义新单位与**该类别基准单位(Base Unit)**之间的数学关系。- 对于大多数物理量,使用
UnitConverterLinear
即可,它表示一个简单的线性关系。 coefficient
参数指定了 “1 个新单位等于多少个基准单位”。在我们的例子中,UnitMass
的基准单位是kilograms
,所以coefficient: 0.5
意味着 1jin
= 0.5kilograms
。
- 对于大多数物理量,使用
进阶提示:对于单位之间的转换不仅需要系数,还需要一个偏移量( 例如温度单位 ,它们的转换公式是
y = ax + b
的形式),Foundation 提供了另一个专门的转换器:UnitConverterTemperature
。它用于处理了这种带有偏移量的线性转换。
自定义全新单位类别
如果系统预设的单位类别,甚至在现有类别上扩展单位,仍然无法满足你的特定需求(比如需要定义一种全新的物理量或者非标准的度量体系),那么我们还可以创建全新的单位类别。
要做到这一点,了解 Foundation 中单位类型的继承结构会很有帮助:
- Unit: 所有单位的基础类。
- Dimension: 可度量单位的抽象基类,继承自
Unit
。它专门用于表示那些可以通过线性转换(乘以系数)相互关联的单位(如长度、质量、时间等)。这是实现单位换算的核心。 - UnitLength, UnitMass, UnitDuration 等: Foundation 预设的具体单位类别,它们都继承自
Dimension
,并定义了各自领域内的具体单位(如米、千克、秒)及其与基准单位的关系。
因此,创建自定义单位类别的关键就是:声明一个继承自 Dimension 的新类。
下面我们以中国传统的质量单位(斤、两、钱)为例,演示如何自定义一个完整的 UnitType:
/// 中国重量单位:斤、两、钱
final class UnitChineseMass: Dimension, @unchecked Sendable {
// 定义单位:斤(基准单位)、两、钱
static let jin = UnitChineseMass(symbol: "斤", converter: UnitConverterLinear(coefficient: 1)) // 以“斤”作为基准 (coefficient: 1)
static let liang = UnitChineseMass(symbol: "两", converter: UnitConverterLinear(coefficient: 0.1)) // 1 两 = 0.1 斤
static let qian = UnitChineseMass(symbol: "钱", converter: UnitConverterLinear(coefficient: 0.01)) // 1 钱 = 0.01 斤
// 指定这个类别的基准单位,所有转换都将基于它
// 这里我们选择“斤”作为基准单位
override class func baseUnit() -> UnitChineseMass {
return .jin
}
}
let weight = Measurement(value: 1, unit: UnitChineseMass.jin)
weight.converted(to: .liang).value // 10
weight.converted(to: .qian).value // 100
自定义的单位类别同样支持 Measurement
的所有运算和格式化特性(但格式化的本地化符号可能需要额外处理)。
善用 Foundation,事半功倍
正如我们所见,Measurement API 体现了 Foundation 框架多年积累的智慧:设计精良、考虑周全且内建国际化支持。这提醒我们,在着手实现许多看似基础的功能之前,养成先查阅 Foundation 文档的习惯,往往能带来惊喜。你很可能会找到一个官方提供、性能更佳、久经考验的解决方案。选择利用这些现成的“轮子”,不仅能显著提升开发效率,也能让代码更加稳健,让我们能真正专注于应用的核心逻辑。毕竟,Less code, more life!
"加入我们的 Discord 社区,与超过 2000 名苹果生态的中文开发者一起交流!"