I'm attempting to store an NSMeasurement
value in SwiftData like so: Store NSMeasurement or NSUnit in Core Data
I'm using both an existing Dimension with a custom unit (UnitLength
Smoot), and a fully custom measurement Dimension (Radioactivity), as directed by Apple:
extension UnitLength {
static let smoot = UnitLength(symbol: "smoot", converter: UnitConverterLinear(coefficient: 1.70180))
}
class UnitRadioactivity: Dimension {
static let becquerels = UnitRadioactivity(symbol: "Bq", converter: UnitConverterLinear(coefficient: 1))
static let curie = UnitRadioactivity(symbol: "Ci", converter: UnitConverterLinear(coefficient: 3.7e10))
override class func baseUnit() -> Self { UnitRadioactivity.becquerels as! Self }
}
I'm using a ValueTransformer via @Attribute(.transformable(by:
to store the value, but open to similar approaches.
@Model
class MeasurementModel {
@Attribute(.transformable(by: MeasurementTransformer.self)) let measurement: NSMeasurement
init(_ measurement: NSMeasurement) {
self.measurement = measurement
}
}
final class MeasurementTransformer: ValueTransformer {
override func transformedValue(_ value: Any?) -> Any? {
guard let measurement = value as? NSMeasurement else { return nil }
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: measurement, requiringSecureCoding: true)
return data
} catch {
debugPrint(error.localizedDescription)
return nil
}
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else { return nil }
do {
let measurement = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSMeasurement.self, from: data)
return measurement
} catch {
debugPrint(error.localizedDescription)
return nil
}
}
override class func allowsReverseTransformation() -> Bool {
true
}
override class func transformedValueClass() -> AnyClass {
NSMeasurement.self
}
}
The transformer works fine for UnitMass
and other built-in Unit types. It also works for the custom smoot
measurement unit for the existing UnitLength
Dimension.
However it does not work for my fully custom Dimension UnitRadioactivity
.
func testMeasurementValueTransformer() throws {
let transformer = MeasurementTransformer()
let mass = NSMeasurement(doubleValue: 42, unit: UnitMass.stones)
let massTransformed = transformer.transformedValue(mass)
let massReverse = transformer.reverseTransformedValue(massTransformed)
let massAgain = try XCTUnwrap(massReverse) as! NSMeasurement
let massUnit = try XCTUnwrap(massAgain.unit) as! UnitMass
XCTAssertEqual(massUnit, UnitMass.stones) // ok
XCTAssertNotEqual(massUnit, UnitMass.kilograms) // ok
let length = NSMeasurement(doubleValue: 364.4, unit: UnitLength.smoot)
let lengthTransformed = transformer.transformedValue(length)
let lengthReverse = transformer.reverseTransformedValue(lengthTransformed)
let lengthAgain = try XCTUnwrap(lengthReverse) as! NSMeasurement
let lengthUnit = try XCTUnwrap(lengthAgain.unit) as! UnitLength
XCTAssertEqual(lengthUnit, UnitLength.smoot) // ok
XCTAssertNotEqual(lengthUnit, UnitLength.meters) // ok
let radiation = NSMeasurement(doubleValue: 1903, unit: UnitRadioactivity.curie)
let radiationTransformed = transformer.transformedValue(radiation) // NSKeyedArchiver error: "The data couldn’t be written because it isn’t in the correct format."
let radiationReverse = transformer.reverseTransformedValue(radiationTransformed)
let radiationAgain = try XCTUnwrap(radiationReverse) as! NSMeasurement // testMeasurementValueTransformer(): XCTUnwrap failed: expected non-nil value of type "Any"
let radiationUnit = try XCTUnwrap(radiationAgain.unit) as! UnitRadioactivity
XCTAssertEqual(radiationUnit, UnitRadioactivity.curie)
XCTAssertNotEqual(radiationUnit, UnitRadioactivity.becquerels)
}
Why doesn't transforming/archiving Measurements work with custom Dimension subclasses?
How can I modify transformation so that the custom Dimension measurement units are retained?
The problem is with the UnitRadioactivity
implementation and partly with the error handling in the value transformer.
If you replace debugPrint(error.localizedDescription)
with debugPrint(error)
what is printed is
Error Domain=NSCocoaErrorDomain Code=4866 "The data couldn’t be written because it isn’t in the correct format." UserInfo={NSUnderlyingError=0x600000b26df0 {Error Domain=NSCocoaErrorDomain Code=4864 "Class '__lldb_expr_88.UnitRadioactivity' has a superclass that supports secure coding, but '__lldb_expr_88.UnitRadioactivity' overrides -initWithCoder: and does not override +supportsSecureCoding. The class must implement +supportsSecureCoding and return YES to verify that its implementation of -initWithCoder: is secure coding compliant." ...
So simply overriding this method removes the thrown error and the transformation works.
class UnitRadioactivity: Dimension {
// existing code ...
override class var supportsSecureCoding: Bool { true }
}
The reverse transformation also seems to work but I am not sure how the result should be properly casted