Adding Module Name to Core Data Entities - swift

I'm having trouble working out the implications of this note from Apple's "Using Swift with Cocoa and Objective-C":
Swift classes are namespaced—they’re scoped to the module (typically, the project) they are compiled in. To use a Swift subclass of the NSManagedObject class with your Core Data model, prefix the class name in the Class field in the model entity inspector with the name of your module.
I've done that, using my own application and just the stock master-detail template, so my entity's name is "Event" and its class is "Stock_Master_Detail.Event". When I then choose Create NSManagedObject Subclass from the Editor menu, and ask it to create a Swift subclass, it doesn't name the class right. Xcode creates a file called "Stock_Master_Detail.swift" with that's for a class called Stock_Master_Detail. And if I create multiple entities, all with the module name prefixed, Xcode can't generate more than one subclass since they'll all wind up having the same name.
I'll add that everything works fine in my limited testing if I just omit the module name entirely, counter to Apple's documentation. My question, then, is, what are the implications of not adding the module name to my class?

One way I got around this is to leave the data model file alone, so in this case the class would be "Event". Then in the code where the NSManagedObjectModel is created (AppDelegate if you used Apple's project template) I changed it to:
lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle(forClass: self.classForCoder).URLForResource("Model", withExtension: "momd")!
var bundleModel = NSManagedObjectModel(contentsOfURL: modelURL)!
var objectModel = NSManagedObjectModel()
objectModel.entities = bundleModel.entities.map { ( entity ) -> NSEntityDescription in
var entityCopy = entity.copy() as NSEntityDescription
entityCopy.managedObjectClassName = "Stock_Master_Detail." + entity.managedObjectClassName
return entityCopy
}
return objectModel
}()
One improvement, I would like to figure out is how to find the module name dynamically. I know I could parse the current class's name, but that feels a weirder than I want.

I believe that it could be a bug with Swift/Core Data. First, generate your managed object without the module name(prefix). After it is generated, then go back and add the prefix in core data.

Related

How to expose CoreData to swift package unit tests?

I'm trying to test CoreData in my swift package as SPM now supports bundled resources including .xcdatamodel, my tests can't seem to locate my NSManagedObjects though. What are the steps to unit test core data from the tests?
I'm getting this error when i try to create a NSManagedObject from a test:
+entityForName: could not locate an entity named 'StriveUser' in this model. (NSInternalInconsistencyException)
I've triple checked the naming and it's all correct.
I'm creating the object like this from my tests:
let object = NSEntityDescription.insertNewObject(forEntityName: "StriveUser", into: self.inMemoryStore.context)
And here's my code for locating the .xcdatamodel:
fileprivate var managedObjectModel: NSManagedObjectModel = {
guard let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main]) else {
preconditionFailure("Error getting ManagedObjectModel")
}
return managedObjectModel
}()
final class InMemoryStore {
let context: NSManagedObjectContext
init() {
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
description.shouldAddStoreAsynchronously = false
let container = NSPersistentContainer(name: Constants.modelName, managedObjectModel: managedObjectModel)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores {_, error in
if let error = error {
fatalError("Failed to load store: \(error.localizedDescription)")
}
}
self.context = container.viewContext
}
}
If you declare a Swift tools version of 5.3 or later in your package manifest, you can bundle resources with your source code as Swift packages. For example, Swift packages can contain asset catalogs, storyboards, and so on.
When resources are defined, a new static Bundle reference is created for the package. This can be accessed using Bundle.module.
So for your ManagedObjectModel you will need update the bundle reference. A good way of using this is to have an accessor in your package that will return the model.
For additional information, you can check out Apple's developer documentation Bundling Resources with a Swift Package.
I ran into a similar issue, where my app would crash with "YourManagedObject not found" errors any time I tried to do anything with my core data models.
This started happening as soon as I moved my core data dependency from cocoapods to swift package manager.
But here was my solution:
add #objc(ManagedObjectName) to all of my NSManagedObject classes
in the core data model editor, in the Data model inspector, delete Current Project Module and use the default for the Module configuration.
As the above answers are saying, make sure to use Bundle.module instead of Bundle.main when loading your NSManagedObjectModel.
I rewrote our core data stack to use NSPersistentStore instead of manually setting everything up (NSPersistentStoreCoordinator, NSManagedObjectContext, etc...) as per WWDC 2018 core data best practices
A few things to note:
The problem wasn't because of the wrong bundle, I had already applied that change when migrating over to SPM. For some reason the app just couldn't find any of my NSManagedObject classes at runtime.
Try #1 and #2 before you try #4. I have no idea if #4 helped with this issue or not as the app stopped crashing when I removed Current Project Module. It definitely cleaned up a lot of ugly legacy code though.
I built a new bare-bones Swift Package to demonstrate the error that seems to be a cause of these issues, as of Xcode Version 13.3.1 (13E500a)
No NSEntityDescriptions in any model claim the NSManagedObject subclass 'TestModel.EntityMO' so +entity is confused. Have you loaded your NSManagedObjectModel yet ?
Breaking just after the model was loaded, I could see the entity existed:
(lldb) po model
(<NSManagedObjectModel: 0x6000015f8d70>) isEditable 1, entities {
Entity = "(<NSEntityDescription: 0x6000001c4b00>) name Entity, managedObjectClassName TestModel_TestModel.EntityMO,
<snip>
The managedObjectClassName seems to be the problem. It's the result of using Current Product Module in the class definition within the model, which appears to be concatenating the package top-level name and the containing folder in Sources. If I replace it with a hard-coded module of TestModel, then the error goes away and the test passes. Not ideal, but it worked in my case.
Xcode support for Core Data in swift packages seems to be a work-in-progress, as the editor still does not load correctly for .xcdatamodeld files. Just creating the test model had to be done in another project and moved to the package since I couldn't add an entity to an empty model file.
For reference, I'll also include my model initialization, which is very basic, but I believe reasonable relative to Apple guidelines. At the very least, it demonstrates that issues can exist beyond Bundle.module usage.
public struct TestModel {
internal static let modelURL = Bundle.module.url(forResource: "Model", withExtension: "momd")!
public static func persistentContainer() -> NSPersistentContainer {
let model = NSManagedObjectModel(contentsOf: modelURL)!
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
let container = NSPersistentContainer(name: "Test", managedObjectModel: model)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { storeDescription, error in
guard error == nil else {
fatalError("Could not load persistent stores. \(error!)")
}
}
return container
}
}

Doctrine - mapping an entity from an external library

i'm using an external library (the awesome nicmart/Tree to build trees) that returns me an object that is the extension of the original object produced by the library
class originalObject
{
//some properties
// this is the object produced by the library
// i dont want to modify the external library so no mapping here
}
class myObject extends originalObject
{
//this is the entity i want to persist
// but it hasnt got any property ??
}
i want to persist myObject with Doctrine\MongoDB, therefore i need to map it.
i (obviously) dont want to modify the library itself so my question is:
where do i put the mapping ?
I thought i could override the properties (like i would do with methods) by re-declaring them and adding the mapping to the re-declaration, but they are not overriden but duplicated.
IMHO you can't do it this way. You'll have to modify originalObject (add annotations there) or you'll have to declare whole myObject and can't inherit origObj.

Core Data: Could not cast value of type 'MyType_MyType_2' to MyType

I have an Objective-C model class MyType. This class is used in Swift code:
NSEntityDescription.insertNewObjectForEntityForName("MyType", inManagedObjectContext: context) as! MyType
The as! cast results in the error message
Core Data: Could not cast value of type 'MyType_MyType_2' (0x7be99a30) to MyType (0xf33db74).
If I cast it as NSManagedObject it works. When I print the result, I can nevertheless see, that it is an actual instance of MyType:
<MyType: 0x7ae06d50> (entity: MyType; id: 0x7aeba6d0 <x-coredata:///MyType/t957F2860-85F8-46E0-B6D6-4D1DF6C4EC613> ; data: {
...the fields...
})
What is happening here? And where does the name MyType_MyType_2 come from?
When I had this issue, it was because I had forgotten to set the "class" on the entity. This is what I came up with:
Click on .xcdatamodelId file in your file structure/project
navigator pane (far left).
Select the entity that you are having issues with.
In the Utilities pane (far right), look for the icon that looks like a 1997 cell phone, the Data Model Inspector.
Under the entity settings, you should see two fields, "Name" and "Class" - set up "Class" with the name of the class you are using (typically the same as "Name").
You'll even notice before you follow these steps that the default "class" is NSObject, reflecting the error message. I found some programatic ways to do this too, but this seemed like the simplest/quickest solution.
I should note that my model WAS written in Swift, so I did have to add the #objc(Entity) interoperability reference mentioned by #zellb. But that shouldn't make a difference in the solution as long as you are doing that part properly (and that would cause a different unrelated error from my understanding).
Set Entity Class Name
Set Module "Current Product Module" like below
just try this:
#objc(MyType)
public class MyType: NSManagedObject {
// your class
}
instead of this:
class MyType: NSManagedObject {
// your class
}
I had mistakenly set a "parent entity" in one of my entities in the data model inspector in the entity section. I mistakenly thought that referred to the destination of a one-to-many relationship.
Setting it back to "no parent entity" fixed the problem, although I did have to delete and reinstall the app in the simulator to deal with the messed up core data database.

Core Data NSManagedObject downcast with XCode 7 and Swift

Running the following code aborts in the return line
Type is not Workout
Could not cast value of type
'NSManagedObject_Workout_' (0x7fcca20620f0) to
'AppName.Workout' (0x100ea5f40)).
The part inside if let... is never executed.
func createWorkoutWithName (name: String) -> Workout? {
let entityName = NSStringFromClass(Workout.classForCoder())
let newEntity = NSEntityDescription.insertNewObjectForEntityForName(entityName, inManagedObjectContext: managedObjectContext)
if let newEntity = newEntity as? Workout {
newEntity.name = name
}
NSLog("createWorkoutWithName: Type is not Workout")
return (newEntity as! Workout)
}
I had this problem in the past and I solved it in XCode 6.x by going in the entity inspector and setting Class = AppName.Workout
One of several answers that suggests this solution is
How come I can cast to NSManagedObject but not to my entity's type?
XCode 7 adds a new twist to this problem:
When I set
Class = AppName.Workout
in the entity inspector, XCode 7 changes the class name automagically to
Class = AppNameWorkout
by removing the dot between AppName and ClassName.
So how can I do this in XCode 7 when I can't set a dot between AppName and ClassName?
Go to your cdatamodel..where you declared your entity..There is a Default Configuration below the Entity. Check the Class column against your entity. By default this will have a dot prefixed. Remove the dot prefix .And it should work. And from what i see is you do not have to prefix your entity class name with the module name in Xcode 7. Hence you cannot use a dot in the class name.I am a newbie in iOS development. So i might not be totally right. But removing the dot prefix solved my issue in Xcode 7.
You need two fixes:
First: Set your entity class name as your entity name. For example:
Entity name = Entity
Class name = Entity
Second: add this code above your entity class file:
#obj(Entity name)
For example:
#obj(Entity)
Class Entity: NSManagedObject {
...
}
The combination of these solutions and the link you provided got me to the solution, but it took me a while to piece it all together. This is what I came up with (and it was basically just an oversight):
Click on .xcdatamodelId file in your file structure/project
navigator pane (far left).
Select the entity that you are having issues with.
In the Utilities pane (far right), look for the icon that looks like a 1997 cell phone, the Data Model Inspector.
Under the entity settings, you should see two fields, "Name" and "Class" - set up "Class" with the name of the class you are using (typically the same as "Name").
Thats where I messed up, even though all my other entities had this set, I had forgotten to set this on this particular entity. You'll even notice before you follow these steps that the default "class" is NSObject, reflecting the error message. Some of the other solutions here likely ultimately do the same thing, but I found this the simplest/quickest solution.
I should note that I already had some of the other items noted set, like the #objc(Entity) interoperability reference.

executeFetchRequest doesn't return the NSManagedObject subclass

This is for XCode 6 and Swift...
I'm trying to make a fetch request to the managed object context but it's not returning the correct subclass.
I have already set the subclass in the data model data modeler configuration to the name of my custom subclass and in the code, it is extending the NSManagedObject class.
Any ideas?
Just figured out the solution.
I had to add the #objc attribute to allow the class to be compatible with Objective-C.
Now the fetch request is returning a correct result of Tasks[]
import Foundation
import CoreData
#objc(Task) // make compatible with objective-c
class Task : NSManagedObject
{
#NSManaged var note: String!
#NSManaged var completed: Bool
}
Reference: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html#//apple_ref/doc/uid/TP40014216-CH4-XID_36
Using #objc(Task) seems to be working but you could also just edit the data model data modeler configuration to the name ToDoList.Task instead of just Task. That will work too and avoid Class conflicts if Task is used anywhere else in the Objective-C code.
Check to make sure that in the "Entity" inspector (right side of the screen, Utilities pane) when Task is selected in your Model that its Class field is properly filled in with "Task" (it's blank by default).