swiftunits-of-measurement

Formatting units and measurements. Why does Units init from symbol not produce same result as static init


Recently I came across a quite curious behavior regarding units and measurements in Swift which I can't understand. After creating measurements with units created with a static UnitMass accessor I can properly format them and receive sensible outputs:

let measurement = Measurement(value: 42.0, unit: UnitMass.kilograms)
measurement.formatted() // -> "42 kg" correct

When creating a measurement using a UnitMass created using init with its symbol I get wrong outputs

let anotherMass = UnitMass(symbol: UnitMass.kilograms.symbol)
let anotherMeasurement = Measurement(value: 42.0, unit: anotherMass)
anotherMeasurement.formatted() // -> "0 μg" wrong

The same happens when using a MeasurementFormatter instead of accessing formatted on the measurement. What is interesting: When debugging this in a playground and inspecting the outputs anotherMass and anotherMeasurement seem to be valid and produce sensible values: screenshot of Swift Playground with collapsed debug info

Complete Playground

import UIKit

let mass = UnitMass.kilograms
let anotherMass = UnitMass(symbol: mass.symbol)

let measurement = Measurement(value: 42.0, unit: mass)
let anotherMeasurement = Measurement(value: 42.0, unit: anotherMass)

let formatted = measurement.formatted()
let anotherFormatted = anotherMeasurement.formatted()

Unfortunately I think I can't just use the static init because I want to store a unit in a SwiftData model and because UnitMass is not Codable I can only store the symbol representation. Therefor I need to work with init(symbol:) when converting back to a Unit.


Solution

  • The init that you are using is declared in Unit. It won't know anything about UnitMass, and won't see that your symbol to be the same symbol as UnitMass.kilograms, and return UnitMass.kilograms. After all, if it worked this way, you wouldn't be able to create new units with the same symbol as existing units. Unit.init just creates a brand new unit with that symbol, unrelated to any other units.

    Also note how Dimension.init(symbol:converter) is used to create your custom unit. See the docs

    The simplest way to define a custom unit is to create a new instance of an existing NSDimension subclass using the init(symbol:converter:) method.

    This presumably also applies to Unit.init(symbol:).

    If you want to store a unit in SwiftData, a workaround would be to store the Data representation of the Unit (which you can get from NSKeyedArchiver since Unit conforms to NSSecureCoding). You can also try storing it as a @Attribute(.transformable) using NSSecureUnarchiveFromDataTransformer like this post shows.

    Alternatively, you can also store a conversion coefficient. You can get this by getting (unit.converter as? UnitConverterLinear).coefficint. Then use Dimension.init(symbol:converter) to recreate your unit, as a custom unit with the same conversion factor. However, you still lose localisation in the formatting this way.