Swift delete core data on background thread not updating MOC

Viewed 450

I’m trying to get my head around background threads and manipulating CoreData via a background MOC, but I’ve ran into a bit of a snag whilst deleting a large number of records from CoreData

My setup is, I have a number of (Main) records, each of which can have 0 to several thousand (Secondary) records associated to them via a one to many relationship

This is represented by 2 view controllers in a master/detail style setup.

ViewController A has a table view that lists all of the main records. ViewController B then shows the associated records when a cell on viewController A is tapped

Both tables are populated on the main thread and the data is fetched using the persistentContainer.viewContext.

ViewController B gives the user the option to delete all of the associated records in bulk, so I would like this to be done on a background thread, so as not to block the main thread.

The problem I am having is, If I delete the records and close viewController B before the process is finished, it doesn’t delete the records. However, it deletes them fine if I leave viewController B open until the delete has finished.

One thing I have noticed, is if I do close ViewController B before the delete thread has finished, the view doesn’t deinitialize until the background delete process ends, so it seems to still be deleting after the view is closed, but it doesn’t seem to save to the persistent store unless I leave the view open until the process has finished

Here is the code i use to delete the records in the background:

class GlobalChanges....
      
static func deleteRecords(records: [NSManagedObject], managedContext: NSManagedObjectContext, finished: () -> Void){
        
    //create a new background MOC
    let coreDataManager = CoreDataStack.shared
    let backgroundContext = coreDataManager.persistentContainer.newBackgroundContext()
    backgroundContext.automaticallyMergesChangesFromParent = true
    
    backgroundContext.performAndWait {
        
        
        //remove the records from the managed context
        for record in records{
            
            let backgroundContextRecord = backgroundContext.object(with: record.objectID) as NSManagedObject
            
            //delete the record
            backgroundContext.delete(backgroundContextRecord)
            
        }
        
        
        do {
            //update core data
            try backgroundContext.save()
            
            managedContext.refreshAllObjects()

            finished()
                      
        } catch let error as NSError  {
            print("Could not save \(error), \(error.userInfo)")
        }
    }
}

And this is called when user taps delete by:

   @IBAction func deleteButton(_ sender: UIButton) {
            
        let deleteQueue = DispatchQueue(label: "deleteQueue")
              
        deleteQueue.async {
            GlobalChanges.deleteRecords(records: self.selectedRows, managedContext: self.managedContext){
                
                DispatchQueue.main.async{
                    //update UI...
                }
            }
        }
    }

How would I get the data to persist when closing ViewController B before the delete process has finished?

Thank in advance

2 Answers

What I would like to contribute to your question is not the solution of your problem per se. But I want to help you improve your approach to "delete all of the associated records in bulk".

If indeed you want to delete all the secondary records of a Primary (Main) records at once, the best approach in CoreData is to edit the to-Many relationship using the Relationship Inspector of the .xcmodeld file and set the Delete Rule to cascade.

Then you delete the primary record itself. Core Data will automatically cascade your delete to all the secondary records. They will be deleted as well without you doing anything else.

If you need the keep the primary record an only clear the relationship, you might consider having a to-One relationship to an intermediate entity that will hold the to-Many secondary records. This way you can delete that intermediate entity an assign a new one to the to-One relationship.

Ok, I’ve sussed it

Each time View controller B is closed, it calculates totals from the remaining (Secondary) records and writes the data back to the (Main) record on the ViewContext.

This was causing a merge conflict due to the backgroundContext and the ViewContext both trying to update the persistent store at the same time.

Update: To get around this, I queued all context writes, so that only one write was being performed on the store at any given time

let persistentContainerQueue = OperationQueue()
persistentContainerQueue.maxConcurrentOperationCount = 1
         
    func SaveBackgroundContext(backgroundContext: NSManagedObjectContext) {
        
        //add the save operation to the back of the queue
        persistentContainerQueue.addOperation(){
        
            backgroundContext.performAndWait{
                
                do {
                    //update core data
                    try backgroundContext.save()
                    
                    
                } catch let error as NSError  {
                    print("Could not save \(error), \(error.userInfo)")
                }
            }
        }
    }
Related