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:
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.
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 theinit(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.