Updating NSFetchedResultsController with the executeBackgroundTask Function

I have an NSFetchedResultsController and am trying to update my data in the background context. For example, here I am trying to delete an object:

 persistentContainer.performBackgroundTask { context in let object = context.object(with: restaurant.objectID) context.delete(object) try? context.save() } 

There are two things that I do not understand:

  • I expected this to change , but not preserve the parent context. However, the parent context is definitely preserved (as verified by manually opening the SQLite file).
  • I would expect the NSFetchedResultsController be updated when the background content NSFetchedResultsController up its parent, but this does not happen. Do I need to manually start something in the main thread?

Obviously I'm not getting something. Can anyone explain this?

I know that I have correctly implemented the methods of delegating result controllers, because if I change my code to directly update viewContext , everything will work as expected.

+5
source share
3 answers

Description

NSPersistentContainer instance methods performBackgroundTask(_:) and newBackgroundContext() poorly documented.

No matter which method you call, in any case, the (returned) temporary NSManagedObjectContext configured using privateQueueConcurrencyType and is directly related to NSPersistentStoreCoordinator and therefore does not have a parent .

See documentation :

Calling this method causes the persistent container to create and return a new NSManagedObjectContext with the concurrencyType parameter set to privateQueueConcurrencyType. This new context will be directly associated with NSPsistentStoreCoordinator and configured to consume NSManagedObjectContextDidSave is automatically translated.

... or confirm yourself:

 persistentContainer.performBackgroundTask { (context) in print(context.parent) // nil print(context.persistentStoreCoordinator) // Optional(<NSPersistentStoreCoordinator: 0x...>) } let context = persistentContainer.newBackgroundContext() print(context.parent) // nil print(context.persistentStoreCoordinator) // Optional(<NSPersistentStoreCoordinator: 0x...>) 

Due to the lack of parent changes will not be bound to the parent context , for example, for example. viewContext and with viewContext intact, the connected NSFetchedResultsController does not recognize any changes and therefore does not update or call its delegate methods. Instead, the changes will be transferred directly to the persistent store coordinator and then saved to the persistent store .

I hope that I was able to help you, and if you need more help, I can add how to get the desired behavior, as described by you, to my answer. ( Solution added below)

Decision

You achieve the behavior you described using two NSManagedObjectContext with a parent-child relationship:

 // Create new context for asynchronous execution with privateQueueConcurrencyType let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) // Add your viewContext as parent, therefore changes are pushed to the viewContext, instead of the persistent store coordinator let viewContext = persistentContainer.viewContext backgroundContext.parent = viewContext backgroundContext.perform { // Do your work... let object = backgroundContext.object(with: restaurant.objectID) backgroundContext.delete(object) // Propagate changes to the viewContext -> fetched results controller will be notified as a consequence try? backgroundContext.save() viewContext.performAndWait { // Save viewContext on the main queue in order to store changes persistently try? viewContext.save() } } 

However, you can also use performBackgroundTask(_:) or use newBackgroundContext() . But, as said earlier, in this case the changes are saved in the permanent storage directly, and viewContext not updated by default. To propagate the changes to viewContext , which triggers the NSFetchedResultsController notification, you must set viewContext.automaticallyMergesChangesFromParent to true :

 // Set automaticallyMergesChangesFromParent to true persistentContainer.viewContext.automaticallyMergesChangesFromParent = true persistentContainer.performBackgroundTask { context in // Do your work... let object = context.object(with: restaurant.objectID) context.delete(object) // Save changes to persistent store, update viewContext and notify fetched results controller try? context.save() } 

Note that extensive changes, such as adding 10,000 objects at the same time, most likely make your NSFetchedResultsController crazy and therefore block the main queue .

+18
source

This works great in my project. In the updateEnglishNewsListener (:) function, here the parameter data is in any object, and I will further convert it to json formate to save the goal.

Core Data uses a thread (or serialized queue) restriction to protect managed objects and managed object contexts (see Master Data Programming Guide). The consequence of this is that the context assumes that the owner by default is the thread or queue that assigned it - this is determined by the thread that invokes its init method. Therefore, you should not initialize the context in one thread, and then pass it to another thread.

There are three types 1. ConfinementConcurrencyType 2. PrivateQueueConcurrencyType 3. MainQueueConcurrencyType

The MainQueueConcurrencyType type creates a context associated with the main queue, which is ideal for use with NSFetchedResultsController.

In the updateEnglishNewsListener (:) function, the params data is your input. (data-> The data you want to update.)

  private func updateEnglishNewsListener(data: [AnyObject] ){ //Here is your data let privateAsyncMOC_En = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) // The context is associated with the main queue, and as such is tied into the application's event loop, but it is otherwise similar to a private queue-based context. You use this queue type for contexts linked to controllers and UI objects that are required to be used only on the main thread. privateAsyncMOC_En.parent = managedObjectContext privateAsyncMOC_En.perform{ // The perform(_:) method returns immediately and the context executes the block methods on its own thread. Here it use background thread. let convetedJSonData = self.convertAnyobjectToJSON(anyObject: data as AnyObject) for (_ ,object) in convetedJSonData{ self.checkIFNewsIdForEnglishAlreadyExists(newsId: object["news_id"].intValue, completion: { (count) in if count != 0{ self.updateDataBaseOfEnglishNews(json: object, newsId: object["news_id"].intValue) } }) } do { if privateAsyncMOC_En.hasChanges{ try privateAsyncMOC_En.save() } if managedObjectContext.hasChanges{ try managedObjectContext.save() } }catch { print(error) } } } 

Data validation already exists in coredata or not to avoid data redundancy. Coredata does not have a primary key concept, so we sequentially check the data that already exists in coredata or not. Data is updated if and only if a data update already exists in coredata. Here checkIFNewsIdForEnglishAlreadyExists (:) function returns 0 or value. If it returns 0, the data is not stored in the saved database. I use a completion descriptor to know new data or old data.

  private func checkIFNewsIdForEnglishAlreadyExists(newsId:Int,completion:(_ count:Int)->()){ let fetchReq:NSFetchRequest<TestEntity> = TestEntity.fetchRequest() fetchReq.predicate = NSPredicate(format: "news_id = %d",newsId) fetchReq.fetchLimit = 1 // this gives one data at a time for checking coming data to saved data do { let count = try managedObjectContext.count(for: fetchReq) completion(count) }catch{ let error = error as NSError print("\(error)") completion(0) } } 

Replacing old data with new data as required.

  private func updateDataBaseOfEnglishNews(json: JSON, newsId : Int){ do { let fetchRequest:NSFetchRequest<TestEntity> = TestEntity.fetchRequest() fetchRequest.predicate = NSPredicate(format: "news_id = %d",newsId) let fetchResults = try managedObjectContext.fetch(fetchRequest as! NSFetchRequest<NSFetchRequestResult>) as? [TestEntity] if let fetchResults = fetchResults { if fetchResults.count != 0{ let newManagedObject = fetchResults[0] newManagedObject.setValue(json["category_name"].stringValue, forKey: "category_name") newManagedObject.setValue(json["description"].stringValue, forKey: "description1") do { if ((newManagedObject.managedObjectContext?.hasChanges) != nil){ try newManagedObject.managedObjectContext?.save() } } catch { let saveError = error as NSError print(saveError) } } } } catch { let saveError = error as NSError print(saveError) } } 

Convert anyobject to JSON to save target in coredata p>

  func convertAnyobjectToJSON(anyObject: AnyObject) -> JSON{ let jsonData = try! JSONSerialization.data(withJSONObject: anyObject, options: JSONSerialization.WritingOptions.prettyPrinted) let jsonString = NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue)! as String if let dataFromString = jsonString.data(using: String.Encoding.utf8, allowLossyConversion: false) { let json = JSON(data: dataFromString) return json } return nil } 

Hope this helps you. If there is any confusion, please ask.

0
source

The viewing context will not be updated unless you set it to automatically merge the changes with the parent. The viewContext is already set as a child of any backgroundContext that you get from the NSPsistentContainer.

Try adding only one line:

 persistentContainer.viewContext.automaticallyMergesChangesFromParent = true 

Now updating viewContext WILL after saving the backgroundContext and this WILL will start NSFetchedResultsController to update.

0
source

Source: https://habr.com/ru/post/1260597/


All Articles