Update NSFetchedResultsController using performBackgroundTask

前端 未结 3 1244
粉色の甜心
粉色の甜心 2020-12-13 10:21

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



        
相关标签:
3条回答
  • 2020-12-13 10:53

    The view context will not update unless you have set it to automatically merge changes from the parent. The viewContext is already set as child of any backgroundContext that you receive from the NSPersistentContainer.

    Try adding just this one line:

    persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
    

    Now, the viewContext WILL update after the backgroundContext has been saved and this WILL trigger the NSFetchedResultsController to update.

    0 讨论(0)
  • 2020-12-13 11:00

    This works me perfectly in my project. In function updateEnglishNewsListener(:), here parameter data is in anyobject and i further convert it into json formate for saving purpose.

    Core Data uses thread (or serialized queue) confinement to protect managed objects and managed object contexts (see Core Data Programming Guide). A consequence of this is that a context assumes the default owner is the thread or queue that allocated it—this is determined by the thread that calls its init method. You should not, therefore, initialize a context on one thread then pass it to a different thread.

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

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

    In updateEnglishNewsListener(:) function, params data is your input. (data->Data you wants 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)
                    }
                }
        }
    

    Checking data is already exist in coredata or not for avoiding the redundance data. Coredata have not primary key concept so we sequently check data already exist in coredata or not. Data is updated if and only if updating data is already exist in coredata. Here checkIFNewsIdForEnglishAlreadyExists(:) function return 0 or value . If it returns 0 then data is not saved in database else saved. I am using completion handle for knowing the 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 the old data to new one according to requirement.

        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)
            }
        }
    

    Convertion anyobject to JSON for saving purpose in coredata

        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 it will help you. If any confusion then please ask.

    0 讨论(0)
  • 2020-12-13 11:18

    Explanation

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

    No matter which method you call, in either case the (returned) temporary NSManagedObjectContext is set up with privateQueueConcurrencyType and is associated with the NSPersistentStoreCoordinator directly and therefore has no parent.

    See documentation:

    Invoking this method causes the persistent container to create and return a new NSManagedObjectContext with the concurrencyType set to privateQueueConcurrencyType. This new context will be associated with the NSPersistentStoreCoordinator directly and is set to consume NSManagedObjectContextDidSave broadcasts automatically.

    ... or confirm it 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 a parent, changes won't get committed to a parent context like e.g. the viewContext and with the viewContext untouched, a connected NSFetchedResultsController won’t recognize any changes and therefore doesn’t update or call its delegate's methods. Instead changes will be pushed directly to the persistent store coordinator and after that saved to the persistent store.

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

    Solution

    You achieve the behavior, as described by you, by using two NSManagedObjectContexts 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 stick with performBackgroundTask(_:) or use newBackgroundContext(). But as said before, in this case changes are saved to the persistent store directly and the viewContext isn't updated by default. In order to propagate changes down to the viewContext, which causes NSFetchedResultsController to be notified, you have to 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()
    }
    

    Please note that extensive changes such as adding 10.000 objects at once will likely drive your NSFetchedResultsController mad and therefore block the main queue.

    0 讨论(0)
提交回复
热议问题