Core Data & Xcode 11: Please switch to using "NSSecureUnarchiveFromData" or a subclass of NSSecureUnarchiveFromDataTransformer - swift

Just moved to Xcode 11 and getting the following crash at launch:
CoreData: fault: 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.
CoreData: warning: Property 'color' on Entity 'Group' is using nil or an insecure NSValueTransformer. Please switch to using "NSSecureUnarchiveFromData" or a subclass of NSSecureUnarchiveFromDataTransformer instead.
I'm creating an NSPersistentContainer at launch using the code below:
private let container: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyApp", managedObjectModel: MyAppModelVersion.current.managedObjectModel())
let storeDescription = NSPersistentStoreDescription(url: getStoreURLWithUserName())
storeDescription.shouldMigrateStoreAutomatically = true
storeDescription.shouldInferMappingModelAutomatically = true
container.persistentStoreDescriptions = [storeDescription]
return container
}()
Error occurs right after this line is executed:
let container = NSPersistentContainer(name: "MyApp", managedObjectModel: MyAppModelVersion.current.managedObjectModel())
I also have a property called 'Colorin aGroup` entity that's transformable:
#NSManaged public var color: UIColor?
#NSManaged public var hexColorValue: String?
Below is how set the property:
public var hexColor: String? {
get {
return self.hexColorValue
}
set {
self.hexColorValue = newValue
if let str = newValue {
self.color = UIColor(hex: str)
}
}
}
This is what the property looks like in Core Data:
I am not sure how to recover from this crash. This was working fine with Xcode 10

Setting Transformer property to NSSecureUnarchiveFromDataTransformer solved the warning in my case. For this select the attribute & set its transformer type to NSSecureUnarchiveFromDataTransformer & run again by pressing commond+R.
Thanks,
Ratneshwar

Swift 5.4.2
This worked for me.
EDIT Link to the article is here.
Click on the .xcdatamodeld file in the project navigator
Click on the
Entity that has a Transformable Attribute
Click on the Transformable Attribute
Click the 'Show Data Model Inspector' icon
Enter 'NSSecureUnarchiveFromDataTransformer'
in the Transformer field - EDIT Make this 'NSSecureUnarchiveFromData' as of Swift 5.5.2.
Your warnings/errors should go away. If not, try cleaning your build folder and rebuild.

This is related to a migration from NSCoding to the NSSecureCoding protocol. The default ValueTransformer adopts NSCoding, so the only solution that worked for me was to write my own Transformer that adopts the NSSecureUnarchiveFromDataTransformer protocol.
I should say that my own experience is from trying to define an Attribute with the Transformer type to persist a custom class that adopted NSCoding. I was initially met with a warning similar to the OP's error. I was able to suppress the warning by changing the Transformer field on the attribute to "NSSecureUnarchiveFromData" as others have mentioned, but I then received an error along the lines of:
Not able to save to CoreData. SQLCore dispatchRequest Object of class “ ” not among allowed top level class list... as mentioned here. The suggestion to change the Attribute to a Relationship was undesirable in my case.
More digging came up with this blog post that details the "reason" for all of this, and gives a solution that worked for me. The blog actually uses the case of UIColor in the example, but it works for any custom class as well.
Say you have a CustomClass that you want to store as a Transformable Attribute in some Entity. If you're like me, you may have adopted NSCoding and received the aforementioned error. The solution would be to adopt NSSecureCoding instead and define an NSSecureUnarchiveFromDataTransformer subclass:
#objc(CustomClassValueTransformer)
final class CustomClassValueTransformer: NSSecureUnarchiveFromDataTransformer {
static let name = NSValueTransformerName(rawValue: String(describing: CustomClass.self))
// Make sure `CustomClass` is in the allowed class list,
// AND any other classes that are encoded in `CustomClass`
override static var allowedTopLevelClasses: [AnyClass] {
// for example... yours may look different
return [CustomClass.self, OtherClass.self, NSArray.self, NSValue.self]
}
/// Registers the transformer.
public static func register() {
let transformer = CustomClassValueTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
}
Then make sure to set the Transformer field on your Attribute to "CustomClassValueTransformer" and the Custom Class field to "CustomClass" and you should be good to go.

For Objective-C and iOS 14, the following solution works for UIColor attributes.
First add a new subclass of NSSecureUnarchiveFromDataTransformer
#interface ColorValueTransformer : NSSecureUnarchiveFromDataTransformer
Add the following static method to your implementation file:
#implementation ColorValueTransformer
+ (NSArray<Class> *)allowedTopLevelClasses {
return #[UIColor.class];
}
#end
Open your data model (e.g. datamodel..xcdatamodeld)
Select the entity and the related attribute which needs the new Transformer
Open the Data Model Inspector
Add the class name (e.g. ColorValueTransformer) as Transformer to that attribute
Change the Custom Class to UIColor
Build and run…

For the transformable attribute, you need to set its type in the Custom Class field.
For instance, I have a transformable field which stores an array of numbers and its Custom Class is declared as [Int16]. This is most likely the cause of the crash. And as #vadian mentioned before, you don't need both fields.
After your crash is fixed, you can get rid of the warning by setting the Transformer field to NSSecureUnarchiveFromData (you simply type this into the field)

I received the same warning messages when updating to Xcode 11, however in my case they are just warnings, but no crash.
In order to work out the best solution, I tried creating a stripped down sample app with just a single entity containing a transformable attribute. But it seems that no matter what I tried I could not reproduce the problem. The I copied the model file from my main app to the demo app, and of course that failed.
So I got to the point where I just had two model files and a simple unit test which does nothing more than open the model and create a persistent store container:
func testDataModels() {
openDataModel(named: "samplemodel")
openDataModel(named: "appmodel")
}
func openDataModel(named name: String) {
print("Opening \(name)")
guard let url = findFile(forResource: name, withExtension: "momd"),
let managedObjectModel = NSManagedObjectModel(contentsOf: url)
else {
XCTFail("Unable to find \(name) data model")
return
}
print(url)
_ = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
}
func findFile(forResource name: String, withExtension ext: String) -> URL? {
if let url = Bundle(for: type(of: self)).url(forResource: name, withExtension: ext) {
return url
}
return Bundle.main.url(forResource: name, withExtension: ext)
}
The appmodel causes the error messsages but the sample model does not. Even when I stripped the appmodel down to a single Entity it continues to generate the errors.
Comparing the contents of the samplemodel with the appmodel (show package contents in Finder), there is a hidden file called .xccurrentversion in the samplemodel but not in the appmodel. The file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>samplemodel.xcdatamodel</string>
</dict>
</plist>
So I created a similar file for the appmodel and put it in the package folder.
Surprisingly, that silences the warning messages! Conversely, deleting the .xccurrentversion file from the samplemodel causes the error messages to be generated. This will allow testing of the problem in isolation.
So this may be a short term fix. In the meantime, I need to work out how to migrate to secure coding.

Maybe some of these answers are acceptable enough, in my case I went to this post. I hope it will be of much help.

Related

SwiftUI FileDocument using Class instead of Struct

I've started a document-base macOS app in SwiftUI and am using a FileDocument (not a Reference FileDocument) as the type of document. In every tutorial I've seen, even in Apple's own WWDC video discussing it (https://developer.apple.com/wwdc20/10039), a struct is always used to define the FileDocument.
My question is: is there an issue with using a class in the struct defining the document. Doing so doesn't result in any Xcode warnings but I wanted to be sure I'm not creating any issues for my app before going down this path.
Below is some example code for what I'm talking about: declaring TestProjectData as a class for use within the DocumentDataAsClassInsteadOfStructDocument - struct as a FileDocument?
public class TestProjectData: Codable{
var anotherString: String
init(){
anotherString = "Hello world!"
}
}
struct DocumentDataAsClassInsteadOfStructDocument: FileDocument, Codable {
var project: TestProjectData
init() {
project = TestProjectData()
}
static var readableContentTypes: [UTType] { [.exampleText] }
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let _ = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
let fileContents = try JSONDecoder().decode(Self.self, from: data)
self = fileContents
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = try JSONEncoder().encode(self)
return .init(regularFileWithContents: data)
}
}
It appears that yes, we need to use a struct for documents. See this post for a thorough example of the issues you can run into if you use a class instead of a struct.
SwiftUI View doesn't update
SwiftUI's architecture is all about using value types for speed and consistency. E.g. on a state change we create all the View structs and then SwiftUI diffs and uses the result to init/update/deinit UIView objects.
I believe the same thing happens with FileDocument. The struct is diffed on a change and the difference is used to init/update/deinit a UIDocument object.
If you init object vars inside these structs then basically it is a memory leak because a new object will be init every time the struct is created which is every time something changes. Also chances are you'll end up using the wrong instance of the object because there will be so many. You can see this type of problem surface when blocks are used inside body, the callback usually happens on an older version of the View struct, which isn't a problem when everything is value types but it is a big problem if referencing old objects.
Try to stick to value types in SwiftUI if you can, if you use objects you'll run into all kinds of headaches. And I don't think ReferenceFileDocument even works yet - I seem to remember it needs some kind of undo manager workaround.

Swift Core Data - storing an array of custom types

I am trying to create a data model which mirrors a view model that I use to handle an API call, the idea being that I will be able to store all the necessary data in core data and then access it when the user is offline, effectively giving the app offline functionality.
However, there is one entity which I need to store which is an array of a custom class that I have in the app:
[OrderSheet]
This is a struct defined as follows:
struct OrderSheet {
let order: SheetClass // codable class
let sheet: Sheet // codable struct
init(fuelOrder: SheetClass, sheet: Sheet) {
self.order = order
self.sheet = sheet
}
}
How can I create an entity that would be capable of storing the above?
One simple way would be to have an entity that holds only one Data field (Binary Data in xcdatamodel settings), which would be the orderSheet itself.
Before going with this solution, I'd like to mention that, one down side of this approach is; if later in the future, any of the models inside OrderSheet changes, you won't be able to retrieve already stored objects as conversion will fail. One way of overcoming this issue would be declaring everything inside OrderSheet and sub models as Optional. But if it is not so crucial, meaning, if not being able to read old models on user's device after an app update is okay, (maybe they will be replaced with new networking call) then you can go with not marking properties as optional either.
Lets imagine you create an entity named OrderSheetManaged with one field as I mentioned like following:
import Foundation
import CoreData
#objc(Entity)
public class OrderSheetManaged: NSManagedObject {
}
extension OrderSheetManaged {
#nonobjc public class func fetchRequest() -> NSFetchRequest<OrderSheetManaged> {
return NSFetchRequest<OrderSheetManaged>(entityName: "OrderSheetManaged")
}
#NSManaged public var orderSheet: Data?
}
I will write some code for NSManagedObjectContext, which is not directly related to your question, you should make research on how to initialise a core data stack and a managed context from it if you are not familiar with that since it is crucial.
I also do some force unwrapping for simplicity, make sure to not force unwrap where not needed in production code.
Now whenever you have an actual OrderSheet object (it is orderSheet in my example below), that was parsed before, you are going to convert it to Data and persist it with new Core Data model as following:
// unrelated to question, it should already be initialised from core data stack, I just init with
// concurrency type to make compiler happy, dont do this before further research.
let yourManagedContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let entityDescription = NSEntityDescription.entity(forEntityName: "OrderSheetManaged",
in: yourManagedContext)
let dataForCoreData = try! JSONEncoder().encode(orderSheet)
let managedOrderSheet = NSManagedObject(entity: entityDescription!, insertInto: yourManagedContext)
managedOrderSheet.setValue(dataForCoreData, forKey: "orderSheet")
Now we have persisted your object as Data inside a wrapper core data model (OrderSheetManaged)
Let's see now how we can fetch these models from our core data and convert it back to OrderSheet model:
// when you fetch it
var orderSheets = [OrderSheet]()
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "OrderSheetManaged")
var coreDataObjects: [NSManagedObject]!
do {
coreDataObjects = try yourManagedContext.fetch(request) as? [NSManagedObject]
for coreDataObject in coreDataObjects {
if let orderSheetData = coreDataObject.value(forKey: "orderSheet") as? Data {
let orderSheet = try! JSONDecoder().decode(OrderSheet.self, from: orderSheetData)
orderSheets.append(orderSheet)
}
}
} catch {
error
}
Now you will have all your stored order sheets inside orderSheets array.
You can also write some utility methods to easily modify core data models by converting orderSheet data inside of them to OrderSheet first and then again converting it back to Data after modifying and then persisting again with setValue.

NSKeyedUnarchiver decodeObjectForKey: cannot decode object of class for key (NS.objects)

I looked through whole SO but still no answer. My app reports this problem:
Fatal Exception: NSInvalidUnarchiveOperationException
*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (App_Title.Products) for key (NS.objects); the
class may be defined in source code or a library that is not linked
I did already do NSKeyedUnarchiver.setClass just before #unarchiveObject:
func loadProducts() -> [Products]? {
NSKeyedUnarchiver.setClass(Products.self, forClassName: "Products")
let unarchivedData = NSKeyedUnarchiver.unarchiveObject(withFile: Products.ArchiveURL.path)
My Products class begins with #Objc:
import Foundation
#objc(Products)
class Products: NSObject, Codable, NSCoding { ... }
Adding the two lines above which seemed to help people didn't bring me any luck. Before them and after them it's the same behaviour. I personally could never reproduce this issue myself.
During development I keep very closely to the app guide on peristance and reviewed it multiple times.
Just before NSKeyedArchiver I check for file existance:
let filePath = Products.ArchiveURL.path
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
Here some additional informations in screenshots.
The only place where I could find a real exception description was Firebase Crashalytics:
A screenshot from the Xcode Organizer under the Crash tab:
Which leads to this line in the code:
NSKeyedUnarchiver.setClass(Products.self, forClassName: "Products")
let unarchivedData = NSKeyedUnarchiver.unarchiveObject(withFile: Products.ArchiveURL.path)
Products class with #Objc annotation.
It seems that you are mixing up Codable and NSCoding. Don't try to use both simultaneously. NSKeyedUnarchiver belongs to NSCoding.
Your class contains property list compliant properties. Drop NSCoding and use only Codable. (By the way it's recommended to name the class in singular form Product).
Delete everything which is related to NSCoding including the protocol conformance and with Codable it's not necessary that the class must inherit from NSObject (the object can be even a struct).
The loadProducts function can be reduced to
func loadProducts() throws -> [Product] {
let data = try Data(contentsOf: Product.ArchiveURL)
return try PropertyListDecoder().decode([Product].self, from: data)
}
It's good practice to hand over thrown errors to the caller
And delete the CodingKeys, you don't need them if the keys match the property names.

NSManagedObject changes do not trigger objectWillChange

I have a Core Data model with an entity generated into class Task. I am trying to get the Combine publisher objectWillChange from the NSManagedObject to send (automatically, without manual work), but it won't. The task entity has a name attribute.
let task = Task(context: container.viewContext)
let taskSubscription = task.objectWillChange.sink(receiveValue: { _ in
print("Task changed")
})
task.name = "Foo" // WILL NOT trigger
If I call send manually, the subscription will work:
task.objectWillChange.send() // Will trigger
If I replace this with a simple ObservableObject, it will work as expected:
class DummyTask: ObservableObject {
#Published var name: String?
}
let dummy = DummyTask()
let dummySubscription = dummy.objectWillChange.sink(receiveValue: { _ in
print("Dummy changed")
})
dummy.name = "Foo" // Will trigger
dummy.objectWillChange.send() // Will trigger
Is NSManagedObject bugged? How should I observe the general entity object for changes? How should I get SwiftUI to see them?
This is using Xcode 11.0 and iOS 13.
I believe it is a bug. There is no point for NSManagedObject to conform to ObservableObject but unable to mark any property as #Published.
While we are waiting Apple to rectify this, I've come across a cleaner solution than #jesseSpencer's suggested one. The logic behind is the same, by adding a objectWillChange.send(), but adding globally into willChangeValue(forKey key: String) instead of adding into individual properties.
override public func willChangeValue(forKey key: String) {
super.willChangeValue(forKey: key)
self.objectWillChange.send()
}
Credits: https://forums.developer.apple.com/thread/121897
My guess is that it is a bug. NSManagedObject conformance to ObservableObject was added in beta 5 which also introduced other significant changes, including deprecation of BindableObject (for replacement by ObservableObject).
See the SwiftUI section:
https://developer.apple.com/documentation/ios_ipados_release_notes/ios_13_release_notes)
I ran into the same issue and despite NSManagedObject conforming to ObservableObject, it was not emitting notifications for changes. This might have something to do with NSManagedObject properties needing to be wrapped with #NSManaged which cannot be combined with #Published, while the ObservableObject doc states that, by default, an ObservableObject will synthesize objectWillChange publishers for #Published property changes. https://developer.apple.com/documentation/combine/observableobject
I first tried to get around this by bootstrapping a call to objectWillChange.send() in overrides to Key-Value methods in my NSManagedObject subclass, which only resulted in incorrect behavior.
The solution I went with is the simplest and unfortunately maybe the bulkiest if you need to change a lot of codependent properties in your SwiftUI view. But, so far it is working fine for me and maintains use of SwiftUI as intended.
In Swift:
Create an NSManagedObject subclass for your entity.
In that subclass, create setter methods for the properties you wish to change from your SwiftUI views and at the beginning of the method add a call to objectWillChange.send(), which should look something like this:
func setTitle(_ text: String) {
objectWillChange.send()
self.title = text
}
I only advise this as a temporary workaround, as it is not ideal and hopefully will be addressed soon.
I will be submitting a bug report in FeedbackAssistant and I recommend to anyone else encountering this issue to do the same, so we can get Apple to take another look at this!
Edit:
A warning about #Anthony’s answer:
While the suggested approach does work, be aware that it will not work when changing collection type relationships, i.e. adding an object to an array associated with the NSManagedObject.
To observe NSManagedObject changes, please look at: https://developer.apple.com/documentation/combine/performing-key-value-observing-with-combine.
Pay attention that, when you use that method in custom class of UICollectionViewListCell or UITableViewCell, you should override the prepareForReuse method and put there alike code:
override func prepareForReuse() {
super.prepareForReuse()
nameObserver?.cancel()
nameObserver = nil
}

Downcasting NSManagedObject in Swift Core Data

I have a class that inherits from NSManagedObject. I'm using this object for model data and it's also being persisted.
class Foo: NSManagedObject {
#NSManaged var firstVar: String
#NSManaged var secondVar: String
let entity = NSEntityDescription.entityForName("Foo", inManagedObjectContext: managedObjectContext)
let createdManagedObject = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: managedObjectContext) as? Foo
}
I can create the object and get the NSManagedObject instance, but I can't downcast it to actual class.
Actually, if I downcast it form optional value, then I will get nil value and if I downcast with unwrapping then it will crash.
When downcast with nil value debugger inspector shows type: Module.Foo, which I believe is the root of the problem.
Of course I tried with naming the class in .xcmodel inspector, tried to name Entity as Module.Foo, but the latter is not allowed as of Xcode 7 any more anyway.
All together: no success. Now I have to access Foo object through KV, which is kind of awkward.
Anyone solved this issue yet?
EDIT: Adding the code to show creation and down-casting. I had issues with Xcode 7.0 and now same with 7.1.1
It seems that the main problem was the target. Thanks to Martin R for pointing me to the problem.
Problem was, that also the code in the main app was part or another target, similar to tests. When object was retrieved from Core Data, it's class signature was Target.Foo and if you tried to cast it to Foo in Target2, you got the error message that cast from Target.Foo to Target2.Foo is not possible and unfortunately this message didn't show in a log unless the log was empty ??
It doesn't matter at all, how you name the Class or Module if at all in setting below:
The only thing that matters is that your .xcmodel is in the same target than the code that accesses it.
You need to edit the data model configuration to tell CoreData which class to use for a specific entity. The actual entity name itself has no impact on the instantiated class.
You need to add the objc annotation to your class:
#objc(Foo)
class Foo: NSManagedObject {
...
}
You need to instantiate the Foo-Class directly:
let createdManagedObject = Foo(entity: entity!, insertIntoManagedObjectContext: managedObjectContext)
If you still want to encapsule the code to manage CoreData, you could do something similar to the following:
static func newInstanceForEntityName(name: String, context: NSManagedObjectContext) -> NSManagedObject {
let anyType: AnyObject.Type = NSClassFromString(name)!
let nsmType: NSManagedObject.Type = anyType as! NSManagedObject.Type
let entity = NSEntityDescription.entityForName(name, inManagedObjectContext: context)
return nsmType.init(entity: entity!, insertIntoManagedObjectContext: context)
}
You can use it like:
let foo = newInstanceForEntityName("Foo", context: context) as! Foo