1

I have an app that heavily uses Measurements. It is written in SwiftUI/iOS 14. I persist the data in a Core Data database. The app is working fine, until I try to implement secure coding. I made a sample app to strip everything away except the basics. It is based off of Xcode's default Core Data app with few changes. Again, I can save my measurement fine until I try to use secure coding.

The xddatamodel, as well as the errors, is as follows: enter image description here

I am using a manual codegen for the Entity and it looks like this:

extension Item {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
        return NSFetchRequest<Item>(entityName: "Item")
    }

    @NSManaged public var timestamp_: Date?
    @NSManaged private var notes_: String?
    @NSManaged private var length_: Measurement<UnitLength>?

    public var timestamp: Date {
        get { timestamp_ ?? Date(timeIntervalSince1970: 0) }
        set { timestamp_ = newValue }
    }

    public var notes: String {
        get { notes_ ?? "Add Notes Here..." }
        set { notes_ = newValue }
    }

    public var length: Measurement<UnitLength> {
        get { length_ ?? Measurement(value: 0, unit: UnitLength.meters) }
        set { length_ = newValue }
    }

}

While I have tried registering the ValueTransformer in different places, I currently have it in the @main:

@main
struct Core_Data_TestApp: App {
    let persistenceController: PersistenceController
    
    init() {
        UnitLengthValueTransformer.register()
        persistenceController = PersistenceController.shared
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

My ValueTransformer looks like this:

@objc(UnitLengthValueTransformer)
final class UnitLengthValueTransformer: NSSecureUnarchiveFromDataTransformer {
    static let name = NSValueTransformerName(rawValue: String(describing: UnitLengthValueTransformer.self))
    override static var allowedTopLevelClasses: [AnyClass] {
        return [UnitLength.self]
    }
    
    public static func register() {
        let transformer = UnitLengthValueTransformer()
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }
}

As I know there has been disagreement, the class header for UnitLength is:

open class UnitLength : Dimension, NSSecureCoding 

so it does conform to NSSecureCoding.

I have used the following references:

Transformable and NSKeyedUnarchiveFromData

CoreData Transformable and NSSecureCoding in iOS 13+

ValueTransformer in Core Data explained: Storing absolute URLs

and I still get the same errors:

Fatal error: Unresolved error Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred.", [:]: file Core_Data_Test/ContentView.swift, line 57

I can't figure out what I am doing wrong that is causing the crash.

Yrb
  • 8,103
  • 2
  • 14
  • 44

1 Answers1

0

Thanks to @LeoDabus I was finally able to hammer this out. The key is to transform the Swift Measurements into NSMeasurement and use NSMeasurement in Core Data. You will need to set your custom class to an NSMeasurement. Your transformer will be a custom value transformer. I called mine NSMeasurementValueTransformer. Your .xcdatamodeld will look like this:

enter image description here I defined my custom value transformer as:

@objc(NSMeasurementValueTransformer)
final class NSMeasurementValueTransformer: NSSecureUnarchiveFromDataTransformer {
    static let name = NSValueTransformerName(rawValue: String(describing: NSMeasurementValueTransformer.self))
    override static var allowedTopLevelClasses: [AnyClass] {
        return [NSMeasurement.self]
    }
    
    public static func register() {
        let transformer = NSMeasurementValueTransformer()
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }
}

Interestingly, I didn't have to register the value transformer. Maybe someone can explain why, but it works without registration.

Lastly, in an extension, I declared a public facing variable that handles the casting of the variable from and to the Measurement I want to use like this:

extension Item {
      public var length: Measurement<UnitLength> {
        // The getter returns an actual value, not an optional. If you want to return an optional, just use:
        // get { length_ as Measurement<UnitLength> }
        get { length_ as Measurement<UnitLength>? ?? Measurement(value: 0, unit: UnitLength.meters) }
        set { length_ = newValue as NSMeasurement }
    }

}

You will notice that the Core Data Attribute is named length_. I can still use Swift Measurements in my app, but store them securely in Core Data. That fixes the Core Data warning:

CoreData: One or more models in this application are using transformable properties with transformer names that are either unset, or set to NSKeyedUnarchiveFromDataTransformerName. Please switch to using “NSSecureUnarchiveFromData” or a subclass of NSSecureUnarchiveFromDataTransformer instead. At some point, Core Data will default to using “NSSecureUnarchiveFromData” when nil is specified, and transformable properties containing classes that do not support NSSecureCoding will become unreadable.

Yrb
  • 8,103
  • 2
  • 14
  • 44