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)
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
?
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.
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.
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.
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 = "" }
}
}
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With