ios - query - Actualice NSFetchedResultsController utilizando performBackgroundTask
swift 4 core data relationships (3)
Tengo un NSFetchedResultsController
y estoy tratando de actualizar mis datos en un contexto de fondo. Por ejemplo, aquí estoy tratando de eliminar un objeto:
persistentContainer.performBackgroundTask { context in
let object = context.object(with: restaurant.objectID)
context.delete(object)
try? context.save()
}
Hay 2 cosas que no entiendo:
- Hubiera esperado que esto modificara, pero no guardara el contexto principal. Sin embargo, el contexto primario definitivamente se guarda (como se verificó al abrir manualmente el archivo SQLite).
- Hubiera esperado que
NSFetchedResultsController
actualizase cuando el contenido de fondo se guarda nuevamente en su copia principal, pero esto no está sucediendo. ¿Debo activar manualmente algo en el hilo principal?
Obviamente, hay algo que no entiendo. ¿Alguien puede explicar esto?
Sé que he implementado los métodos de delegado del controlador de resultados obtenidos correctamente, porque si cambio mi código para actualizar directamente el viewContext
, todo funciona como se esperaba.
Esto me funciona perfectamente en mi proyecto. En la función updateEnglishNewsListener (:), aquí los datos de parámetros están en anyobject y luego los convierto en json formato para propósitos de ahorro.
Los datos principales usan confinamiento de subprocesos (o cola serializada) para proteger los objetos administrados y los contextos de objetos gestionados (consulte la Guía de programación de datos básicos). Una consecuencia de esto es que un contexto asume que el propietario predeterminado es el hilo o la cola que lo asignó, esto está determinado por el hilo que llama a su método init. Por lo tanto, no debe inicializar un contexto en un hilo y luego pasarlo a un hilo diferente.
Hay tres tipos 1. ConfinementConcurrencyType 2. PrivateQueueConcurrencyType 3. MainQueueConcurrencyType
MainQueueConcurrencyType crea un contexto asociado a la cola principal que es perfecto para usar con NSFetchedResultsController.
En la función updateEnglishNewsListener (:), los datos de params son su entrada. (datos-> Datos que quiere actualizar)
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)
}
}
}
La comprobación de datos ya existe en coredata o no para evitar los datos de redundancia. Los Coredatos no tienen el concepto de clave primaria, por lo que verificamos en forma secuencial si los datos ya existen en Coredata o no. Los datos se actualizan solo si la actualización de datos ya existe en coredata. Aquí la función checkIFNewsIdForEnglishAlreadyExists (:) devuelve 0 o valor. Si devuelve 0, entonces los datos no se guardan en la base de datos más guardada. Estoy usando el control de finalización para conocer los datos nuevos o viejos.
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)
}
}
Reemplazar los datos antiguos por uno nuevo de acuerdo con los requisitos.
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)
}
}
Convierta anyobject a JSON para guardar en 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
}
Espero que te ayude. Si hay alguna confusión, por favor pregunte.
Explicación
NSPersistentContainer
métodos de instancia performBackgroundTask(_:)
y newBackgroundContext()
están poco documentados.
NSManagedObjectContext
método al que llame, en cualquier caso el NSManagedObjectContext
temporal (devuelto) se configura con privateQueueConcurrencyType
y está asociado con NSPersistentStoreCoordinator
directamente y, por lo tanto, no tiene parent
.
Ver documentación :
La invocación de este método hace que el contenedor persistente cree y devuelva un nuevo NSManagedObjectContext con concurrencyType establecido en privateQueueConcurrencyType. Este nuevo contexto se asociará directamente con NSPersistentStoreCoordinator y está configurado para consumir transmisiones NSManagedObjectContextDidSave automáticamente.
... o confirme usted mismo:
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...>)
Debido a la falta de un parent
, los cambios no se comprometerán con un parent context
como, por ejemplo, el viewContext
y con viewContext
no viewContext
, un NSFetchedResultsController
conectado no reconocerá ningún cambio y, por lo tanto, no actualiza ni llama a los métodos de su delegate
. . En su lugar, los cambios se enviarán directamente al persistent store coordinator
y luego se guardarán en la persistent store
.
Espero que pueda ayudarlo y, si necesita más ayuda, puedo agregar cómo puedo obtener el comportamiento deseado, como lo describió usted, en mi respuesta. ( Solución agregada a continuación)
Solución
NSManagedObjectContext
el comportamiento, tal como lo describe usted, utilizando dos NSManagedObjectContext
con una relación padre-hijo:
// 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()
}
}
Sin embargo, también puede seguir con performBackgroundTask(_:)
o usar newBackgroundContext()
. Pero como se dijo anteriormente, en este caso los cambios se guardan directamente en la tienda persistente y viewContext
no se actualiza de forma predeterminada. Para propagar los cambios al viewContext
, que hace que se notifique NSFetchedResultsController
, debe establecer viewContext.automaticallyMergesChangesFromParent
en 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()
}
Tenga en cuenta que los cambios extensos, como agregar 10.000 objetos a la vez, probablemente harán que su NSFetchedResultsController
loco y, por lo tanto, bloqueará la main queue
.
El contexto de vista no se actualizará a menos que lo haya configurado para fusionar automáticamente los cambios desde el elemento primario. ViewContext ya está configurado como secundario de cualquier fondoContext que reciba del NSPersistentContainer.
Intenta agregar solo esta línea:
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
Ahora, viewContext se actualizará después de que backgroundContext se haya guardado y esto ACTIVARÁ el NSFetchedResultsController para actualizar.