How to set up 2 core data stacks that use the same in-memory persistent store?

147 views Asked by At

Setup:

My app uses core data & cloud kit mirroring.
For unit tests, I want to mock iCloud mirroring by setting cloudKitContainerOptions = nil of the NSPersistentStoreDescription of the persistent store used.
To mock mirroring, I want to setup a 2nd core data stack that uses the same persistent store as the normal data stack.
Additionally, the SQL persistent store is replaced by an NSInMemoryStoreType persistent store.

Problem:

I did not manage to use the same in-memory persistent store for 2 core data stacks.
Both stacks are created with a NSPersistentCloudKitContainer.
Both use the same NSPersistentStoreDescription with the same file URL, although this file URL is apparently ignored for an in-memory persistent store.
Thus, both containers use different in-memory persistent stores, and it is not possible to mock iCloud mirroring to a single persistent store.

Question:

I the intended setup possible, and if so, how?

PS: I know that I probably could use the same SQL store by specifying the same file UrL. But this had the disadvantage that the store persisted between different unit tests, and had to be reset at the beginning of each test.

1

There are 1 answers

0
Reinhard Männer On

It is indeed possible to set up 2 core data stacks that use the same in-memory persistent store, but they will have not all properties as an SQLite store.
Here is my test setup for the 2 core data stacks:

        let coreDataCloudKitContainer = CoreDataCloudKitContainer(name: appName, privateStoreType: .persistentStore)
//      let coreDataCloudKitContainer = CoreDataCloudKitContainer(name: appName, privateStoreType: .nullDevice)
//      let coreDataCloudKitContainer = CoreDataCloudKitContainer(name: appName, privateStoreType: .inMemory)
        coreDataManager.persistentContainer = coreDataCloudKitContainer
        let privateStore = coreDataCloudKitContainer.privateStore!
        let mockStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: CoreDataCloudKitContainer.managedObjectModel)
        _ = try! mockStoreCoordinator.addPersistentStore(type: NSPersistentStore.StoreType(rawValue: privateStore.type), 
                                                         configuration: privateStore.configurationName, 
                                                         at: privateStore.url!,
                                                         options: [NSPersistentHistoryTrackingKey: true])
        
        let viewContext = coreDataCloudKitContainer.viewContext
        let mockViewContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        mockViewContext.persistentStoreCoordinator = mockStoreCoordinator

CoreDataCloudKitContainer is a subclass of NSPersistentCloudKitContainer. It has a static var managedObjectModel that is used during init of a CoreDataCloudKitContainer as well as the init of the 2nd NSPersistentStoreCoordinator (the model must only be loaded once or core data will get confused):

static var managedObjectModel: NSManagedObjectModel = {
    guard let modelFile = Bundle.main.url(forResource: appName, withExtension: "momd") else { fatalError("Canot find model file") }
    guard let model = NSManagedObjectModel(contentsOf: modelFile) else { fatalError("Cannot parse model file") }
    return model
}()  

Note also that addPersistentStore uses an option [NSPersistentHistoryTrackingKey: true] that is required if privateStore is defined with privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey). If it is not set, one will get an error CoreData: fault: Store opened without NSPersistentHistoryTrackingKey but previously had been opened with NSPersistentHistoryTrackingKey - Forcing into Read Only mode store at 'file://...

privateStore is initialized as

let privateStoreURL: URL
switch privateStoreType {
    case .persistentStore:
        privateStoreURL = CoreDataCloudKitContainer.appDefaultDirectoryURL.appendingPathComponent("Private.sqlite")
    case .nullDevice, .inMemory:
        privateStoreURL = URL(fileURLWithPath: "/dev/null")
}
print("privateStoreURL: \(privateStoreURL)")
let privateStoreDescription = NSPersistentStoreDescription(url: privateStoreURL)
privateStoreDescription.url = privateStoreURL
privateStoreDescription.configuration = privateConfigurationName
privateStoreDescription.timeout = timeout
privateStoreDescription.type = type
privateStoreDescription.isReadOnly = isReadOnly
privateStoreDescription.shouldAddStoreAsynchronously = shouldAddStoreAsynchronously
privateStoreDescription.shouldInferMappingModelAutomatically = shouldInferMappingModelAutomatically
privateStoreDescription.shouldMigrateStoreAutomatically = shouldMigrateStoreAutomatically
// The options below have to be set before loadPersistentStores
// Enable history tracking and remote notifications
privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if isTesting {
    privateStoreDescription.cloudKitContainerOptions = nil
} else {
    privateStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: kICloudContainerID)
    privateStoreDescription.cloudKitContainerOptions!.databaseScope = .private
}  

where type is NSSQLiteStoreType for privateStoreType == .persistentStore and .nullDevice, and NSInMemoryStoreType for privateStoreType == .inMemory. Note that the option .nullDevice creates an SQLite store in memory only, but .inMemory creates some store in memory, but it is not a SQLite store. Infos about a .nullDevice store can be found in this blog.

A warning:
Although it is possible to set up 2 core data stacks using the same in-memory persistent store, such a store has limitations. One is that modifying the store by one stack does not trigger an NSPersistentStoreRemoteChangeNotification, so that unit testing of iCloud mirroring is not possible. For that, one has to use a file based SQLite persistent store.