В этом приложении мы рассматриваем как правильно работать с Core Data
Все что вы делаете на главном потоке должно происходить на главном контексте mainContext или viewContext. Все что вы делаете или хотите сделать в бэкграунд потоке вы должны делать в privateContext
Пример
Model.coreData.backgroundTask { privateContext in
privateContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
for data in array {
let cdUser = CDUser(context: privateContext)
cdUser.fill(data: data)
}
Model.coreData.save(in: privateContext) { status in
DispatchQueue.main.async {
switch status {
case .hasNoChanges, .saved:
completion(nil)
default:
completion(StorageError.saveFailure(CDUser.entityName).NSError)
}
}
}
}В этом примере мы сделали backgroundTask и получили privateContext. Далее мы создаем пользователя на этом контексте, сохраняем и переходим на главный поток.
Допустим вы создали экран редактирования объекта с viewContext. На этом экране вы нажимаете кнопку и открывается новый экран список объектов для выбора на ваш экран. Допустим экран где список выбора тоже есть viewContext но это другой viewContext. Так вот запрещено передавать объект с разных контекстов напрямую. Вместо передачи самого объекта нужно передавать objectID.
Пример
func updateUser(cdUser: CDUser) {
guard let cdUserInContext = self.viewContext.object(with: cdUser.objectID) as? CDUser else { return }
self.cdPost.cdUser = cdUserInContext
self.updateUserCompletion?()
}В этом примере мы передали CDUser но мы не обращаемся к его свойствам и не присваиваем его как параметр как свойство CDPost. Вместо этого мы получаем objectID и воссоздаем объект на нужном нам контексте и только после этого мы можем присвоить пользователя для модели CDPost.
Предположим вы работаете на экране редактирования вашей модели CDPost и вы используете viewContext. В это время прилетел пуш и у вас где то сработал код и в privateContext обновил вашу модель CDPost.
В этом случае когда вы будете сохранить ваш viewContext у вас будет ошибка Error Domain=App.NSError Code=0 "Could not merge changes." UserInfo={NSLocalizedDescription=Could not merge changes.}
Установить для вашего viewContext свойство self.viewContext.automaticallyMergesChangesFromParent = true. Тогда когда сохраниться privateContext все изменения обновятся на вашем viewContext.
Установить для вашего viewContext свойство self.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump. Тогда когда будет сохранятся viewContext то он перезапишет изменения сделанные на privateContext, при этом изменения на privateContext сохранятся если вы их не меняли на viewContext.
Перед сохранением вашего viewContext нужно обновить контекст self.viewContext.refreshAllObjects() или self.viewContext.refresh(self.cdPost, mergeChanges: true)
3.2 Когда вы сохранили что то в privateContext работая в это же время на viewContext. Тут есть дополнение когда вы сохранили используя BatchResuest.
Model.coreData.backgroundTask { privateContext in
guard let cdPost = privateContext.object(with: cdPost.objectID) as? CDPost else { return }
guard let id = cdPost.id else { return }
let predicate = NSPredicate(format: "id == %@", id)
let updateRequest = NSBatchUpdateRequest(entityName: CDPost.entityName)
updateRequest.predicate = predicate
updateRequest.propertiesToUpdate = ["mark": !cdPost.mark]
updateRequest.resultType = .updatedObjectIDsResultType
do {
let results = try privateContext.execute(updateRequest) as! NSBatchUpdateResult
let changes: [AnyHashable: Any] = [
NSUpdatedObjectsKey: results.result as! [NSManagedObjectID]
]
completion(nil)
} catch {
completion(error.NSError)
}
}Установить для вашего viewContext свойство self.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump. Тогда когда будет сохранятся viewContext то он перезапишет изменения сделанные на privateContext, при этом изменения на privateContext сохранятся если вы их не меняли на viewContext.
Когда вы делаете BatchResuest то это пишется сразу в базу данных миную Notification и automaticallyMergesChangesFromParent в этом случае на работает. Нужно сделать Merge вручную
Model.coreData.backgroundTask { privateContext in
guard let cdPost = privateContext.object(with: cdPost.objectID) as? CDPost else { return }
guard let id = cdPost.id else { return }
let predicate = NSPredicate(format: "id == %@", id)
let updateRequest = NSBatchUpdateRequest(entityName: CDPost.entityName)
updateRequest.predicate = predicate
updateRequest.propertiesToUpdate = ["mark": !cdPost.mark]
updateRequest.resultType = .updatedObjectIDsResultType
do {
let results = try privateContext.execute(updateRequest) as! NSBatchUpdateResult
let changes: [AnyHashable: Any] = [
NSUpdatedObjectsKey: results.result as! [NSManagedObjectID]
]
if !contexts.isEmpty {
Log.debug("CoreData", "start merging")
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: contexts)
Log.debug("CoreData", "end merging")
}
completion(nil)
} catch {
completion(error.NSError)
}
}Обратите тут внимание на код
if !contexts.isEmpty {
Log.debug("CoreData", "start merging")
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: contexts)
Log.debug("CoreData", "end merging")
}Мы передаем массив контектов которые мы хотим уведомить об изменениям. В этом массиве должен быть наш viewContext.