Multiple NSEntityDescriptions Claim NSManagedObject Subclass

Post-automatic-caching

This should not happen anymore with NSPersistent[CloudKit]Container(name: String), since it seems to cache the model automatically now (Swift 5.1, Xcode11, iOS13/MacOS10.15).

Pre-automatic-caching

NSPersistentContainer/NSPersistentCloudKitContainer does have two constructors:

The first is just a convenience initializer calling the second with a model loaded from disk. The trouble is that loading the same NSManagedObjectModel twice from disk inside the same app/test invocation results in the errors above, since every loading of the model results in external registration calls, which print errors once called a second time on the same app/test invocation. And init(name: String) was not smart enough to cache the model.

So if you want to load a container multiple time you have to load the NSManagedObjectModel once and store it in an attribute you then use on every init(name:managedObjectModel:) call.

Example: caching a model

import Foundation
import SwiftUI
import CoreData
import CloudKit

class PersistentContainer {
    private static var _model: NSManagedObjectModel?
    private static func model(name: String) throws -> NSManagedObjectModel {
        if _model == nil {
            _model = try loadModel(name: name, bundle: Bundle.main)
        }
        return _model!
    }
    private static func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
        guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
            throw CoreDataError.modelURLNotFound(forResourceName: name)
        }

        guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
            throw CoreDataError.modelLoadingFailed(forURL: modelURL)
       }
        return model
    }

    enum CoreDataError: Error {
        case modelURLNotFound(forResourceName: String)
        case modelLoadingFailed(forURL: URL)
    }

    public static func container() throws -> NSPersistentCloudKitContainer {
        let name = "ItmeStore"
        return NSPersistentCloudKitContainer(name: name, managedObjectModel: try model(name: name))
    }
}

Old answer

Loading Core Data is a little bit of magic, where loading a model from disk and using it means it registers for certain types. A second loading tries to register for the type again, which obviously tells you that something registered for the type already.

You can load Core Data only once and cleanup that instance after each test. Cleanup means deleting every object entity and then saving. There is some function which gives you all entities which you can then fetch and delete. Batch delete is not available InMemory though so object-by-managed object it is there.

The (probably simpler) alternative is to load the model once, store it somewhere and reuse that model on every NSPersistentContainer call, it has a constructor to use a given model instead of loading it again from disk.


In the context of unit tests with an in-memory store, you end up with two different models loaded:

  • The model loaded in your application by the main Core Data stack
  • The model loaded in your unit tests for the in-memory stack.

This causes a problem because apparently + [NSManagedObjectModel entity] looks at all available models to find a matching entity for your NSManagedObject. Since it finds two models, it will complain.

The solution is to insert your object in the context with insertNewObjectForEntityForName:inManagedObjectContext:. This will take in account the context (and as a consequence, the context's model) to look for the entity model and as a consequence limit its search to a single model.

To me it seems to be a bug in the NSManagedObject init(managedObjectContext:) method which seems to rely on +[NSManagedObject entity] rather than relying on the context's model.


As @Kamchatka pointed out, the warning is displayed because NSManagedObject init(managedObjectContext:) is used. Using NSManagedObject initWithEntity:(NSEntityDescription *)entity insertIntoManagedObjectContext:(NSManagedObjectContext *)context dismisses the warning.

If you don't want to use the later constructor in your test, you can just simply create NSManagedObject extension in your test target to override the default behaviour:

import CoreData

public extension NSManagedObject {

    convenience init(usedContext: NSManagedObjectContext) {
        let name = String(describing: type(of: self))
        let entity = NSEntityDescription.entity(forEntityName: name, in: usedContext)!
        self.init(entity: entity, insertInto: usedContext)
    }

}

I found it here, so full credit should go to @shaps


I encountered this issue when trying to do CoreData related unit testing with following aims:

  • in-memory type NSPersistentContainer stack for speed
  • re-create stack for every test cases to wipe data

As Fabian's answer, the root cause of this problem is managedObjectModel being loaded multiple times. However, there might be several possible places of managedObjectModel loading:

  1. In App
  2. In test cases, every setUp calls of XCTestCase subclasses which try to re-create NSPersistentContainer

So it's two folds to solve this issues.

  1. Don't set up NSPersistentContainer stack in app.

You can add a underTesting flag to determine whether to set it up or not.

  1. Load managedObjectModel only once across all unit tests

I use a static variable for managedObjectModel and use it for re-creating in-memory NSPersistentContainer.

Some excerpt as following:

class UnitTestBase {
    static let managedObjectModel: NSManagedObjectModel = {
        let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle(for: UnitTestBase.self)])!
        return managedObjectModel
    }()


    override func setUp() {
        // setup in-memory NSPersistentContainer
        let storeURL = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("store")
        let description = NSPersistentStoreDescription(url: storeURL)
        description.shouldMigrateStoreAutomatically = true
        description.shouldInferMappingModelAutomatically = true
        description.shouldAddStoreAsynchronously = false
        description.type = NSInMemoryStoreType

        let persistentContainer = NSPersistentContainer(name: "DataModel", managedObjectModel: UnitTestBase.managedObjectModel)
        persistentContainer.persistentStoreDescriptions = [description]
        persistentContainer.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Fail to create CoreData Stack \(error.localizedDescription)")
            } else {
                DDLogInfo("CoreData Stack set up with in-memory store type")
            }
        }

        inMemoryPersistentContainer = persistentContainer
    }
}

Above should be suffice for you to fix this issue happens in unit testing.