In everyday life we constantly convert values between different units of measurement. For developers this seems easy—write a few formulas, sprinkle in a couple of switch
statements and you’re done. But the moment you try to support dozens of units, seamless internationalisation, formatting, precision and rounding, the workload sky-rockets and the drudgery can make you question your life choices. The good news: starting with iOS 10 Apple added a comprehensive Measurement API to Foundation, taking all that “donkey work” off our hands. This article walks you through its usage and best practices.
Getting to Know Measurement
Measurement
is a struct that represents a numeric value together with its unit:
value: Double // numeric value
unit : Unit // its unit
Measurement
gives you type-safe unit conversion. Once instantiated it is immutable, following the value-type semantics of struct
.
// A plain number
let height: Double = 180
// Number plus unit
let heightMeasurement = Measurement(value: 180, unit: UnitLength.centimeters)
// Generic syntax, explicitly stating the unit type
let heightMeasurement = Measurement<UnitLength>(value: 180, unit: UnitLength.centimeters)
Compared with storing a bare Double
, Measurement
is clearer semantically and comes with convenient conversion:
// Convert directly to metres
let metres = heightMeasurement.converted(to: .meters).value
print(metres) // 1.8
Foundation ships with dozens of unit categories—length, mass, duration, acceleration, area, temperature and many more—covering almost every everyday need.
Mathematical Operations
As long as two Measurement
values belong to the same unit category (UnitType
), you can perform maths on them—addition, subtraction, comparison, even range definitions:
let heightMeasurement = Measurement<UnitLength>(value: 180, unit: .centimeters)
// Division with a scalar
let half = heightMeasurement / 2
print(half.value) // 90.0
print(half) // "90.0 cm"
// Addition: different units can be added; the result is in the category’s base unit
let h180cm = Measurement<UnitLength>(value: 180, unit: .centimeters)
let h1m = Measurement<UnitLength>(value: 1, unit: .meters)
let totalHeight = h180cm + h1m // 2.8 m (base unit of UnitLength is metres)
print(totalHeight) // "2.8 m"
// Comparison with automatic unit handling
h180cm < h1m // false (180 cm ≮ 1 m)
// Ranges respect units too
let range = h1m ... h180cm
range.contains(Measurement(value: 6, unit: .feet)) // false (~1.83 m is outside)
No manual conversion, unit information retained throughout—readable and safe.
Tip: When you add or subtract two
Measurement
s of the same category, the result is automatically expressed in that category’sbase unit
. We’ll discuss base units shortly.
Formatting
Turning a Measurement
into user-friendly, localised text is a common task. The formatted()
method makes it painless.
The simplest form:
let h180cm = Measurement<UnitLength>(value: 180, unit: UnitLength.centimeters)
h180cm.formatted()
Output varies by the current locale:
- United States (
en-US
) →5.9 ft
(auto-converted to feet) - China (
zh-CN
) →1.8 m
(auto-converted to metres)
Need fine-grained control? Provide a MeasurementFormatStyle
:
let str = h180cm.formatted(
.measurement(
width: .wide, // wide / abbreviated / narrow
usage: .asProvided, // .asProvided / .person / .road / …
numberFormatStyle: .number
.precision(.fractionLength(2)) // numeric formatting
)
)
print(str) // "180.00 centimeters"
Parameter | Description |
---|---|
width | Level of unit detail. wide → “centimeters”, abbreviated → “ cm”, narrow → “cm”. |
usage | Tells the formatter the context so it can pick the most appropriate unit and rounding. For instance .personHeight might render 180 cm as “5 ft 11 in” in imperial regions, whereas .road would aggregate into road-friendly units. |
numberFormatStyle | Full power of FormatStyle for the numeric part—fraction length, grouping, scientific notation, etc. |
The formatter not only converts units based on locale and usage, it translates unit names (e.g. centimeters vs 厘米), giving a truly local experience.
Elegant Display in SwiftUI
Text
’s init(_:format:)
accepts a FormatStyle
, so you can display Measurement
values that automatically react to the environment’s locale:
struct MeasurementDisplay: View {
let h180cm = Measurement<UnitLength>(value: 180, unit: UnitLength.centimeters)
var body: some View {
VStack(alignment: .leading) {
// Example 1: US English, .person context, abbreviated
Text(h180cm, format: .measurement(width: .abbreviated, usage: .person))
.environment(\.locale, .init(identifier: "en_US"))
// Change to .personHeight to show "5 ft, 11 in"
// Example 2: Chinese, .person context, abbreviated
Text(h180cm, format: .measurement(width: .abbreviated, usage: .personHeight))
.environment(\.locale, .init(identifier: "zh_CN"))
// Example 3: French, .road context, wide
Text(h180cm, format: .measurement(width: .wide, usage: .road))
.environment(\.locale, .init(identifier: "fr_FR"))
// Output: "2 mètres" (1.8 m rounded to 2 m for road usage)
}
}
}
If you want different styling for the value and the unit (color, font, weight), format to an AttributedString
first:
struct AttributedMeasurementView: View {
let weight = Measurement(value: 100, unit: UnitMass.kilograms) // 100 kg
// Use `.attributed` to format a Measurement as an AttributedString
var styledWeightAttributedString: AttributedString {
var str = weight.formatted(
.measurement(
width: .abbreviated,
usage: .asProvided,
numberFormatStyle: .number)
.attributed
)
// Precisely control the styles applied to the value and the unit
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)
}
}
For an in-depth look at AttributedString
in SwiftUI, see:
Old Wine in a New Bottle: Bridging with NSMeasurement
Used NSMeasurement
before and wonder if Measurement
is a brand-new Swift implementation? Under the hood they share the same logic:
public struct Measurement<UnitType>: ReferenceConvertible, Comparable, Equatable
where UnitType: Unit { … }
Thanks to ReferenceConvertible
, Measurement
(value type) and NSMeasurement
(reference type) convert seamlessly:
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 is being re-architected in pure Swift, but Measurement hasn’t been rewritten yet. It currently works only on Apple platforms. Other platforms provide a stub implementation so your code still compiles, but these stubs do not perform real conversion or formatting. We look forward to true cross-platform support.
Adding Units to an Existing Category
Sometimes the unit you need belongs to an existing category (mass, length…) but isn’t built-in. Swift’s extension
lets you add it easily.
Example: add the Chinese “jin” (市斤, = 0.5 kg) to UnitMass
:
extension UnitMass {
/// Chinese mass unit: jin
/// 1 jin = 0.5 kilograms
static var jin: UnitMass {
// 1. Symbol
let symbol = NSLocalizedString("斤", comment: "Symbol for the Chinese unit of mass 'jin'")
// 2. Converter
// UnitMass’s base unit is kilograms
let converter = UnitConverterLinear(coefficient: 0.5)
// 3. Create and return
return UnitMass(symbol: symbol, converter: converter)
}
}
let weight = Measurement(value: 1, unit: UnitMass.kilograms)
print(weight.converted(to: .jin).value) // 2.0
symbol
– text representation of the unit.NSLocalizedString
is wise for future localisation (e.g. “Jin” in English).- Limitation: Custom units don’t automatically provide
.wide
,.abbreviated
,.narrow
variants like system units; you usually just get the symbol you supplied.
- Limitation: Custom units don’t automatically provide
converter
– defines the relationship to the base unit of the category.- Most quantities use
UnitConverterLinear
(y = a x). - For units requiring an offset (y = a x + b, e.g. temperature) use
UnitConverterTemperature
.
- Most quantities use
Defining an Entirely New Unit Category
If neither system categories nor extending them suffices—for example you need a brand-new physical quantity or a non-standard system—you can create a custom category.
Understand Foundation’s hierarchy:
Unit
– root of all units.Dimension
– abstract subclass ofUnit
for measurable units related by linear conversion.UnitLength
,UnitMass
,UnitDuration
, … – concrete subclasses ofDimension
with their own units and base units.
So: subclass Dimension
.
Example: traditional Chinese mass units (jin, liang, qian):
/// Chinese mass units: jin, liang, qian
final class UnitChineseMass: Dimension, @unchecked Sendable {
// Units: jin (base), liang, qian
static let jin = UnitChineseMass(symbol: "斤", converter: UnitConverterLinear(coefficient: 1)) // base unit
static let liang = UnitChineseMass(symbol: "两", converter: UnitConverterLinear(coefficient: 0.1)) // 1 liang = 0.1 jin
static let qian = UnitChineseMass(symbol: "钱", converter: UnitConverterLinear(coefficient: 0.01)) // 1 qian = 0.01 jin
// Declare the base unit
override class func baseUnit() -> UnitChineseMass { .jin }
}
let weight = Measurement(value: 1, unit: UnitChineseMass.jin)
weight.converted(to: .liang).value // 10
weight.converted(to: .qian).value // 100
Your custom category enjoys all Measurement
features (though localisation of symbols is up to you).
Make the Most of Foundation—Twice the Result for Half the Effort
As we’ve seen, the Measurement API embodies years of Foundation wisdom: thoughtful design, thorough coverage and built-in internationalisation. Before rolling your own “simple” solution, make a habit of checking Foundation’s docs—you’ll often find an official, battle-tested alternative with better performance. Using these ready-made wheels boosts productivity, hardens your codebase and lets you focus on core logic. After all: Less code, more life!