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 unitMeasurement 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.8Foundation 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
Measurements 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) // cmSwift 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.0symbol– text representation of the unit.NSLocalizedStringis wise for future localisation (e.g. “Jin” in English).- Limitation: Custom units don’t automatically provide
.wide,.abbreviated,.narrowvariants 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 ofUnitfor measurable units related by linear conversion.UnitLength,UnitMass,UnitDuration, … – concrete subclasses ofDimensionwith 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 // 100Your 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!