Search code examples
iosswiftcore-datansmanagedobjectnsmanagedobjectcontext

Why does NSManagedObject not get inserted into a child NSManagedObjectContext when the context is initialised inline?


I am using child NSManagedObjectContexts in my iOS app as a scratchpad. I am using an extension of NSManagedObjectContext to create child contexts, as follows:

extension NSManagedObjectContext {
    func childContext(concurrencyType: NSManagedObjectContextConcurrencyType = .mainQueueConcurrencyType) -> NSManagedObjectContext {
        let childContext = NSManagedObjectContext(concurrencyType: concurrencyType)
        childContext.parent = self
        return childContext
    }
}

I was experiencing repeated errors with the following message for one part of my app when inserting a new NSManagedObject into a new child context (created from the viewContext of a NSPersistentContainer):

CoreData: error: Mutating a managed object after it has been removed from its context

This was strange, as other parts of the app -- which also used child contexts -- worked fine. Eventually, I narrowed the cause down to a small difference in the way I am inserting the new NSManagedObject (of entity name "Book") into the new child context. The following works as expected:

let childContext = container.viewContext.childContext()
let object1 = NSEntityDescription.insertNewObject(forEntityName: "Book", into: childContext)
print(object1.managedObjectContext == nil)
// prints "false"

However, if I create the child context inline (i.e. not assigned to a variable, the managed object is not inserted into the context):

let object2 = NSEntityDescription.insertNewObject(forEntityName: "Book", into: container.viewContext.childContext())
print(object2.managedObjectContext == nil)
// prints "true"

This seems odd to me and can't figure it out. Everything is running on the main thread. I've also created a Swift playground reproducing this behaviour.


Solution

  • Managed objects have a managedObjectContext property, but it's a weak reference. That means that having the managed object doesn't stop the context from being deallocated. If the context gets deallocated, the managedObjectContext property is automatically set to nil.

    In your first code snippet, childContext exists as a local variable in the code that creates the managed object. After you insert the new object, it still exists in the local scope, so the managedObjectContext property still points to the context. The childContext variable keeps a strong reference, so the object still exists.

    In the second snippet, the child context only exists as an argument to insertNewObject. Once that method returns, the context goes out of scope. Since there are no strong references to the context, it gets deallocated, and object2.managedObjectContext is automatically set to nil.