Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CoreData - Store an array of Tranformable objects

I have an entity that has a traformable property. It is an array of custom object, Reminder that confirms to NSSecureCoding.

@objc(Reminder)
public class Reminder: NSObject, NSSecureCoding {
    public static var supportsSecureCoding: Bool = true
    
    
    public var date: Date
    public var isOn: Bool
    
    public  init(date: Date, isOn: Bool) {
        self.date = date
        self.isOn = isOn
    }
    
    struct Keys {
        static var date: String = "date"
        static let isOn: String = "isOn"
    }
    
    public func encode(with aCoder: NSCoder) {
        aCoder.encode(date as NSDate,forKey: Keys.date)
        aCoder.encode(isOn,forKey: Keys.isOn)
    }
    
    required public init?(coder aDecoder: NSCoder) {
        guard let date = aDecoder.decodeObject(of: NSDate.self, forKey: Keys.date) as Date? else {
            return nil
        }
        
        self.date = date
        self.isOn = aDecoder.decodeBool(forKey: Keys.isOn)
    }
}

And the following code is my NSSecureUnarchiveFromDataTransformer.

class ReminderDataTransformer: NSSecureUnarchiveFromDataTransformer {

    override class func allowsReverseTransformation() -> Bool {
        return true
    }

    override class func transformedValueClass() -> AnyClass {
        return Reminder.self
    }

    override class var allowedTopLevelClasses: [AnyClass] {
        return [Reminder.self]
    }

    override func transformedValue(_ value: Any?) -> Any? {
        guard let data = value as? Data else {
            fatalError("Wrong data type: value must be a Data object; received \(type(of: value))")
        }
        return super.transformedValue(data)
    }

    override func reverseTransformedValue(_ value: Any?) -> Any? {
        guard let reminder = value as? [Reminder] else {
            fatalError("Wrong data type: value must be a Reminder object; received \(type(of: value))")
        }
        return super.reverseTransformedValue(reminder)
    }
}



extension NSValueTransformerName {
    static let reminderToDataTransformer = NSValueTransformerName(rawValue: "ReminderToDataTransformer")
}

I have registered ReminderDataTransformer using the following code before initializing NSPersistantContainer.

ValueTransformer.setValueTransformer(ReminderDataTransformer(), forName: .reminderToDataTransformer)

I have used ReminderToDataTransformer as Transformer in xCode's Data Model Inspector.


But it does not work because of the following error that occurs when saving entities.

 [error] error: SQLCore dispatchRequest: exception handling request: <NSSQLSaveChangesRequestContext: 0x282ec0780> , <shared NSSecureUnarchiveFromData transformer> threw while encoding a value. with userInfo of (null)
CoreData: error: SQLCore dispatchRequest: exception handling request: <NSSQLSaveChangesRequestContext: 0x282ec0780> , <shared NSSecureUnarchiveFromData transformer> threw while encoding a value. with userInfo of (null)
2020-12-31 21:44:09.300394+0100 ReminderApp[26406:6247995] [error] error: -executeRequest: encountered exception = <shared NSSecureUnarchiveFromData transformer> threw while encoding a value. with userInfo = (null)

Update

There is no exception. When launching the app for the second time, this error is logged in console.

[error] fault: exception raised during multi-threaded fetch <shared NSSecureUnarchiveFromData transformer> threw while decoding a value. ({
    NSUnderlyingError = "Error Domain=NSCocoaErrorDomain Code=4864 \"value for key 'root' was of unexpected class 'NSArray (0x1fa392238) [/System/Library/Frameworks/CoreFoundation.framework]'. Allowed classes are '{(\n    \"Reminder (0x100fb6920) [/private/var/containers/Bundle/Application/306C3F0B-75AA-4A2D-A934-260B2EB63313/ReminderApp]\”\n)}’.\” UserInfo={NSDebugDescription=value for key 'root' was of unexpected class 'NSArray (0x1fa392238) [/System/Library/Frameworks/CoreFoundation.framework]'. 

I guess I am unable to correctly encode / decode Reminder's array as It works if I change the code to store Reminder instead of [Reminder].

Just to be clear, I can store Reminder, but not [Reminder].


How to store [Reminder] as Transformable?


like image 860
mahan Avatar asked Dec 31 '20 21:12

mahan


People also ask

Can you store an array in Core Data?

There are two steps to storing an array of a custom struct or class in Core Data. The first step is to create a Core Data entity for your custom struct or class. The second step is to add a to-many relationship in the Core Data entity where you want to store the array.

What is transformable in Core Data?

When you declare a property as Transformable Core Data converts your custom data type into binary Data when it is saved to the persistent store and converts it back to your custom data type when fetched from the store. It does this through a value transformer.

How do I save an object in Core Data?

To save an object with Core Data, you can simply create a new instance of the NSManagedObject subclass and save the managed context. In the code above, we've created a new Person instance and saved it locally using Core Data.


2 Answers

If you are going to unarchive an array you have to add NSArray to the top level classes, this is how I understand the error message

override class var allowedTopLevelClasses: [AnyClass] {
    return [NSArray.self, Reminder.self]
}

By the way, instead of a transformable consider to encode the array to JSON and save it as a simple String. The conversion can be performed by a computed property.

The class can be a struct with dramatically less code

public struct Reminder : Codable {
    public var date: Date
    public var isOn: Bool
}

In the NSManagedObject subclass create a String Attribute

@NSManaged public var cdReminders : String

and a computed property

var reminders : [Reminder] {
    get {
       return (try? JSONDecoder().decode([Reminder].self, from: Data(cdReminders.utf8))) ?? []
    }
    set {
       do {
           let reminderData = try JSONEncoder().encode(newValue)
           cdReminders = String(data: reminderData, encoding:.utf8)!
       } catch { cdReminders = "" }
    }
}
like image 97
vadian Avatar answered Oct 27 '22 22:10

vadian


Adding to @vadian's answer, if you want to include the allowed top level classes from the parent data transformer, try

override static var allowedTopLevelClasses: [AnyClass] {
    var allowed = super.allowedTopLevelClasses
    allowed(contentsOf: [Reminder.self])
    
    return allowed
}
like image 1
Tim Newton Avatar answered Oct 27 '22 22:10

Tim Newton