Search code examples
iosswiftcore-data

Core data crash with method "hasChanges"


I have CoreDataStack

I've added debug options for CoreData debugging "-com.apple.CoreData.ConcurrencyDebug 1"

class CoreDataStack {

public enum SaveStatus {
    case saved, rolledBack, hasNoChanges, error
}

private var modelName: String

var viewContext: NSManagedObjectContext

var privateContext: NSManagedObjectContext

var persisterContainer: NSPersistentContainer

init(_ modelName: String) {

    self.modelName = modelName

    let container = NSPersistentContainer(name: modelName)

    container.loadPersistentStores { persisterStoreDescription, error in

        print("CoreData", "Initiated \(persisterStoreDescription)")
        guard error == nil else {
            print("CoreData", "Unresolved error \(error!)")
            return
        }
    }

    self.persisterContainer = container

    self.viewContext = container.viewContext
    self.viewContext.automaticallyMergesChangesFromParent = true

    self.privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    self.privateContext.persistentStoreCoordinator = container.persistentStoreCoordinator
    self.privateContext.automaticallyMergesChangesFromParent = true

}

func createTemporaryViewContext() -> NSManagedObjectContext {
    let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    context.parent = self.privateContext
    context.automaticallyMergesChangesFromParent = true
    return context
}

func saveTempViewContext(tempContext context: NSManagedObjectContext, completion: ((CoreDataStack.SaveStatus) -> Void)? = nil) {

    guard context.hasChanges || privateContext.hasChanges else {
        completion?(.hasNoChanges)
        return
    }

    context.performAndWait {
        do {
            try context.save()
        }
        catch {
            completion?(.error)
            return
        }
    }

    privateContext.perform { [weak self] in
        do {
            try self?.privateContext.save()
            completion?(.saved)
        }
        catch {
            self?.privateContext.rollback()
            completion?(.rolledBack)
        }

    }

}

class ViewController: UIViewController {

@objc
func save(_ sender: UIBarButtonItem) {

    let coreDataStack = CoreDataStack()

    let tempMainContext = coreDataStack.createTemporaryViewContext() //child main context from private context

    var people = People(context: tempMainContext)

    self.people.name = "John Doe"
    self.people.age = 25

    coreDataStack.saveTempViewContext(tempContext: tempMainContext) { status in

        print(status)

    }


}



}

I have "privateContext" attached to coordinator

I've created "tempMainContext" from private context

When I call "saveTempViewContext" I want to save tempMainContext which pushes changes to parent (privateContext) and this privateContext saves to persistent store

So the error occurred in line

privateContext.hasChanges

I know thats this line executes in main thread. And I need to call method "perform" or "performAndWait" to perform on the right queue.

like this

    var contextHasChanges: Bool = false
    var privateContextHasChanges: Bool = false

    context.performAndWait {
        contextHasChanges = context.hasChanges
    }

    privateContext.performAndWait {
        privateContextHasChanges = privateContext.hasChanges
    }

    guard context.hasChanges || privateContext.hasChanges else {
        completion?(.hasNoChanges)
        return
    }

But it so weird to call "performAndWait" just to check that context has changes. And when I call "performAndWait" it block current thread in my case it MainThread. And I don't want block the main thread even for short time.

How could we resolve this issue ?

UPD 1 attached debug stack enter image description here

UPD 2

In my CoreDataStack init method in below I'v added code. I just check if the private context has changes and It will crash

let privateContextHasChanges = privateContext.hasChanges

I think it's because at that line we are in MainThread and we touch private context which init with "privateQueueConcurrencyType" and I investigate that if I touch other property for example "privateContext.name" or "privateContext.parent" it works fine.

But if I touch property like this:

privateContext.hasChanges
privateContext.registeredObjects
privateContext.updatedObjects

maybe other

it will crash again

So I can make a conclusion that these properties are not thread safe.

Can anyone confirm that ?

UPD 3 After I'v read post Unexpected Core Data Multithreading Violation

I'v made conclusions:

  1. If I'm on the main thread and my context's type is .main I do not need any changes and I safe
  2. If I'm on some place and I don't know want kind of thread I'm on I always need to do "perform" or "performAndWait" to synchonize queue attached to context.
  3. Almost always do "perform" and not "performAndWait" except you can't do without it.

Solution

  • context.hasChanges isn'n thread safe. It requires to be used in the context thread. https://developer.apple.com/forums/thread/133378