Changes on Background NSManagedObjectContext not visible on Main, using NSFetchedResultsController - ios

This is a really bizarre issue, and I thought I understood Core Data.
I use a background context that has no parent. Hooked right into the Persistent Store Coordinator. I update objects on this background context then save it. I listen to the ContextDidSaveNotification and merge those changes into my main thread context. Those updated objects are not faults on the main thread as they are already used to populate table view cells. So I would expect those changes to actually merge. But they are not.
Without getting into the details of my data models, it suffices to say that an object has a property "downloadState". Once the parsing work is done on the background thread, downloadStateValue (an enum) gets set to "3", which corresponds to 'completed'.
I subscribe to the ContentWillSave notification now to inspect what's going on. I get this at the end of my parsing work:
2016-06-13 10:19:21.055 MyApp[29162:52855206] Going to save background context.
updated:{(
<QLUserPinnedCourse: 0x7fe195403c10> (entity: QLUserPinnedCourse; id: 0xd0000000002c0002 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLUserPinnedCourse/p11> ; data: {
course = "0xd000000000dc0008 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLCourse/p55>";
courseId = 2794;
/* other fields redacted */
}),
<QLCourse: 0x7fe1954cded0> (entity: QLCourse; id: 0xd000000000dc0008 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLCourse/p55> ; data: {
/* other fields redacted*/
contentDownloadState = 3;
courseId = 2794;
pinnedUserData = "0xd0000000002c0002 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLUserPinnedCourse/p11>";
})
The NSFetchedResultsController that is listenting to QLUserPinnedCourse objects gets the delegate calls, which triggers cell reloads in my tables.
The predicate is:
// Specify criteria for filtering which objects to fetch
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"pinned == %# && course.contentDownloadState IN %#",
#YES,
#[#(QLDownloadStateSucceeded), #(QLDownloadStateNotYetAttempted), #(QLDownloadStateFailed), #(QLDownloadStateIncomplete)]
];
Now when I get to the cell code, I have a QLUserPinnedCourse object to work with. I set a breakpoint in the debugger and get:
(lldb) po userCourse.course
<QLCourse: 0x7fe19568f740> (entity: QLCourse; id: 0xd000000000dc0008 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLCourse/p55> ; data: {
contentDownloadState = 1;
courseId = 2794;
pinnedUserData = "0xd0000000002c0002 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLUserPinnedCourse/p11>";
})
The question is, WHY is contentDownloadState not 3, but still 1 ?? I don't get it.
Shouldn't these changes have been merged??
Details as to my stack:
PSC -> Private Concurrent (saving context) -> Main Thread context
PSC -> Private Concurrent (import context)
ContextDidSave:
if the context was an import context, merge changes into both contexts above:
_contextSaveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification* note)
{
NSManagedObjectContext *contextSaved = note.object;
NSManagedObjectContext *moc = weakself.mainQueueContext;
// basically, if this was a background worker thread
DDLogDebug(#"updatedObjects:%#", note.userInfo[NSUpdatedObjectsKey]);
if ([contextSaved.userInfo[CoreDataUserInfoKeyIsWorkerContext] boolValue])
{
[weakself.privateSavingContext performBlock:^(){
for (NSManagedObject *object in note.userInfo[NSUpdatedObjectsKey]) {
[[weakself.privateSavingContext objectWithID:[object objectID]] willAccessValueForKey:nil];
}
[weakself.privateSavingContext mergeChangesFromContextDidSaveNotification:note];
[moc performBlock:^(){
for (NSManagedObject *object in note.userInfo[NSUpdatedObjectsKey]) {
[[moc objectWithID:[object objectID]] willAccessValueForKey:nil];
}
[moc mergeChangesFromContextDidSaveNotification:note];
}];
}];
}
}];
Note that I'm asking the userCourse.course object for its attribute, although my FRC is interested in QLUserPinnedCourse objects. I thought because I specify a keypath in the predicate that relates to a QLCourse object, these changes are refreshed.

It's a quirk of Core Data. You actually need to re-fault objects in the main context which were updated by the save operation.
Here's an example in Swift:
mainContext.performBlock {
let updatedObjects : Set<NSManagedObject> = notification.userInfo![NSUpdatedObjectsKey] as! Set<NSManagedObject>
for obj in updatedObjects {
self.mainContext.objectWithID(obj.objectID).willAccessValueForKey(nil)
}
self.mainContext.mergeChangesFromContextDidSaveNotification(notification)
}
The main part is the call to willAccessValueForKey:nil, which causes the object to be marked as a fault. This will cause NSFetchedResultsControllers in the main context to fire.

So I found a solution but I can't tell you why it works.
The problem I suppose is that I would have a method 'start downloading content' and I would update the property contentDownloadState on the main thread context to 'incomplete/downloading', then proceed to get all the content.
All the rest of the work was done on a background thread context. When finished I updated that value with 'succeeded'. It wasn't merging that change. I have no idea why.
Once I decided to do everything on the worker context. i.e. change its value then save the context to disk, the changes, ALL the changes were propagating.
So in the end I solved it, but really don't understand the problem.

Related

NSPersistentContainer concurrency for saving to core data

I've read some blogs on this but I'm still confused on how to use NSPersistentContainer performBackgroundTask to create an entity and save it. After creating an instance by calling convenience method init(context moc: NSManagedObjectContext) in performBackgroundTask() { (moc) in } block if I check container.viewContext.hasChanges this returns false and says there's nothing to save, if I call save on moc (background MOC created for this block) I get errors like this:
fatal error: Failure to save context: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x17466c500) for NSManagedObject (0x1702cd3c0) with objectID '0xd000000000100000 <x-coredata://3EE6E11B-1901-47B5-9931-3C95D6513974/Currency/p4>' with oldVersion = 1 and newVersion = 2 and old cached row = {id = 2; ... }fatal error: Failure to save context: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x170664b80) for NSManagedObject (0x1742cb980) with objectID '0xd000000000100000 <x-coredata://3EE6E11B-1901-47B5-9931-3C95D6513974/Currency/p4>' with oldVersion = 1 and newVersion = 2 and old cached row = {id = 2; ...} and new database row = {id = 2; ...}"
)}
So I've failed to get the concurrency working and would really appreciate if someone could explain to me the correct way of using this feature on core data in iOS 10
TL:DR: Your problem is that you are writing using both the viewContext and with background contexts. You should only write to core-data in one synchronous way.
Full explanation: If an object is changed at the same time from two different contexts core-data doesn't know what to do. You can set a mergePolicy to set which change should win, but that really isn't a good solution, because you will lose data that way. The way that a lot of pros have been dealing with the problem for a long time was to have an operation queue to queue the writes so there is only one write going on at a time, and have another context on the main thread only for reads. This way you never get any merge conflicts. (see https://vimeo.com/89370886#t=4223s for a great explanation on this setup).
Making this setup with NSPersistentContainer is very easy. In your core-data manager create a NSOperationQueue
//obj-c
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;
//swift
let persistentContainerQueue = OperationQueue()
persistentContainerQueue.maxConcurrentOperationCount = 1
And do all writing using this queue:
// obj c
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
void (^blockCopy)(NSManagedObjectContext*) = [block copy];
[self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSManagedObjectContext* context = self.persistentContainer.newBackgroundContext;
[context performBlockAndWait:^{
blockCopy(context);
[context save:NULL]; //Don't just pass NULL here, look at the error and log it to your analytics service
}];
}]];
}
//swift
func enqueue(block: #escaping (_ context: NSManagedObjectContext) -> Void) {
persistentContainerQueue.addOperation(){
let context: NSManagedObjectContext = self.persistentContainer.newBackgroundContext()
context.performAndWait{
block(context)
try? context.save() //Don't just use '?' here look at the error and log it to your analytics service
}
}
}
When you call enqueueCoreDataBlock the block is enqueued to ensures that there are no merge conflicts. But if you write to the viewContext that would defeat this setup. Likewise you should treat any other contexts that you create (with newBackgroundContext or with performBackgroundTask) as readonly because they will also be outside of the writing queue.
At first I thought that NSPersistentContainer's performBackgroundTask had an internal queue, and initial testing supported that. After more testing I saw that it could also lead to merge conflicts.

Crashes during CoreData fetching on serial queue

I went through many discutions and subjects about CoreData, but I keep getting the same problem.
Here's the context : I have an application which have to do several access to CoreData. I decided, for simplifying, to declare a serial thread specifically for access (queue.sync for fetching, queue.async for saving). I have a structure that is nested three times, and for recreating the entire structure, I fetch subSubObject, then SubObject and finally Object
But sometimes (like 1/5000 recreation of "Object") CoreData crash on fetching results, with no stack trace, with no crash log, only a
EXC_BAD_ACCESS (code 1)
Objects are not in cause, and the crash is weird because all access are done in the same thread which is a serial thread
If anyone can help me, I will be very grateful !
Here's the structure of the code :
private let delegate:AppDelegate
private let context:NSManagedObjectContext
private let queue:DispatchQueue
override init() {
self.delegate = (UIApplication.shared.delegate as! AppDelegate)
self.context = self.delegate.persistentContainer.viewContext
self.queue = DispatchQueue(label: "aLabel", qos: DispatchQoS.utility)
super.init()
}
(...)
public func loadObject(withID ID: Int)->Object? {
var object:Object? = nil
self.queue.sync {
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Name")
fetchRequest.predicate = NSPredicate(format: "id == %#", NSNumber(value: ID))
do {
var data:[NSManagedObject]
// CRASH HERE ########################
try data = context.fetch(fetchRequest)
// ###################################
if (data.first != nil) {
let subObjects:[Object] = loadSubObjects(forID: ID)
// Task creating "object"
}
} catch let error as NSError {
print("CoreData : \(error), \(error.userInfo)")
}
}
return object
}
private func loadSubObjects(forID ID: Int)->[Object] {
var objects:[Object] = nil
self.queue.sync {
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Name")
fetchRequest.predicate = NSPredicate(format: "id == %#", NSNumber(value: ID))
do {
var data:[NSManagedObject]
// OR HERE ###########################
try data = context.fetch(fetchRequest)
// ###################################
if (data.first != nil) {
let subSubObjects:[Object] = loadSubObjects(forID: ID)
// Task creating "objects"
}
} catch let error as NSError {
print("CoreData : \(error), \(error.userInfo)")
}
}
return objects
}
(etc...)
TL;DR: get rid of your queue, replace it with an operation queue. Run fetches on the main thread with viewContext do writing in one synchronous way.
There are two issues. First is that managedObjectContexts are not thread safe. You cannot access a context (neither for reading or for writing) except from the single thread that it is setup to work with. The second issue is that you shouldn't be doing multiple writing to core-data at the same time. Simultaneous writes can lead to conflicts and loss of data.
The crash is cause by accessing viewContext from a thread that is not the main thread. The fact that there is a queue ensuring that nothing else is accessing core data at the same time doesn't fix it. When core-data thread safety is violated core-data can fail at any time and in any way. That means that it may crash with hard to diagnose crash reports even at points in the code where you are on the correct thread.
You have the right idea that core-data needs a queue to work well when saving data, but your implementation is flawed. A queue for core-data will prevent write conflicts caused by writing conflicting properties to an entity at the same time from different context. Using NSPersistentContainer this is easy to set up.
In your core-data manager create a NSOperationQueue
let persistentContainerQueue : OperationQueue = {
let queue = OperationQueue.init();
queue.maxConcurrentOperationCount = 1;
return queue;
}()
And do all writing using this queue:
func enqueueCoreDataBlock(_ block: #escaping (NSManagedObjectContext) -> Swift.Void){
persistentContainerQueue.addOperation {
let context = self.persistentContainer.newBackgroundContext();
context.performAndWait {
block(context)
do{
try context.save();
} catch{
//log error
}
}
}
}
For writing use enqueueCoreDataBlock: which will give you a context to use and will execute every block inside the queue so you don't get write conflicts. Make sure that no managedObject leave this block - they are attached to the context which will be destroyed at the end of the block. Also you can't pass managedObjects into this block - if you want to change a viewContext object you have to use the objectID and fetch in the background context. In order for the changes to be seen on the viewContext you have to add to your core-data setup persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
For reading you should use the viewContext from the main thread. As you are generally reading in order to display information to the user you aren't gaining anything by having a different thread. The main thread would have to wait for the information in any event so it is faster just the run the fetch on the main thread. Never write on the viewContext. The viewContext does not use the operation queue so writing on it can create write conflicts. Likewise you should treat any other contexts that you create (with newBackgroundContext or with performBackgroundTask) as readonly because they will also be outside of the writing queue.
At first I thought that NSPersistentContainer's performBackgroundTask had an internal queue, and initial testing supported that. After more testing I saw that it could also lead to merge conflicts.

Magical Record add object, different context error

I'm using Magical Record in my app, and want to add the functionality for a user to add a 'Note', which is a child of 'entry'.
I added this code:
[MagicalRecord saveWithBlock: ^(NSManagedObjectContext *localContext) {
Note *newNote = [Note MR_createInContext: localContext];
newNote.content = noteContent;
newNote.name = #"User Note";
[self.entry addNotesObject: newNote];
}
completion: ^(BOOL success, NSError *error) {
if (error != nil)
{
// show alert
}
else if (success)
{
[[self tableView] reloadData];
}
}];
The error I keep getting on the last line is "Illegal attempt to establish a relationship 'entry' between objects in different contexts"
I tried setting the context of both 'entry' and 'newNote' to 'localContext', but I still get the same error.
What am I missing?
self.entry was created in different context, so you can't access it from this one.
Instead of:
[self.entry addNotesObject: newNote];
you should first find self.entry object in localContext:
[[self.entry MR_inContext:localContext] addNotesObject: newNote];
You can find an explanation of using MagicalRecord in a concurrent environment at Performing Core Data operations on Threads. Though it's quite short, so in my opinion it's worthwhile to read Core Data Programming Guide even though you don't use CD directly.

iOS GoogleAnalytic continuously produce an exception

I'm currently using the last version of Google Analytics (v2.0)
I instantiate it the most common way in my appDelegate:
[GAI sharedInstance].trackUncaughtExceptions = NO;
[GAI sharedInstance].dispatchInterval = 0;
[[GAI sharedInstance] trackerWithTrackingId:#"..."];
But when i'm running the app it continuously produce this exception in logs:
An observer of NSManagedObjectContextDidSaveNotification illegally threw an exception. Objects saved = {
deleted = "{(\n <GAIHit: 0xc1cac50> (entity: GAIHit; id: 0xc160740 <x-coredata://8854889C-BE6C-49BB-BBA9-99465B86265E/GAIHit/p26> ; data: {\n dispatchUrl = \"https://ssl.google-analytics.com/collect\";\n gaiVersion = \"2.0b4\";\n parametersData = <62706c69 73743030 d4010203 04050852 53542474 6f705824 6f626a65 63747358 24766572 73696f6e 59246172 63686976 6572>;\n timestamp = \"2013-07-10 10:21:55 +0000\";\n})\n)}";
inserted = "{(\n)}";
updated = "{(\n)}";
} and exception = Object's persistent store is not reachable from this NSManagedObjectContext's coordinator with userInfo = (null)
It doesn't make the application crash, but it's very verbose and pollute my logs.
Moreover, it seems to work, because GA logs says:
-[GAIDispatcher dispatchComplete:withStartTime:withRetryNumber:withResponse:withData:withError:] (GAIDispatcher.m:415) DEBUG: Successfully dispatched hit /GAIHit/p51 (0 retries).
Any idea to stop these logs?
From GAM documentation:
If your app uses the CoreData framework: responding to a notification,
e.g. NSManagedObjectContextDidSaveNotification, from the Google
Analytics CoreData object may result in an exception. Instead, Apple
recommends filtering CoreData notifications by specifying the managed
object context as a parameter to your listener. Learn more from Apple.
I guess that is your case
You should merge changes only from your managed object contexts, not from any other contexts created by the third-party libraries.
However filtering by contexts implies storing somewhere the list of all your background contexts. I've found easier solution: instead of comparing the context with your list of contexts it's sufficient just to check whether the context was created for your PersistentStoreCoordinator:
- (void) managedObjectContextDidSaveNotification: (NSNotification *) notification {
NSManagedObjectContext * context = notification.object;
if (context != self.managedObjectContextForMainThread) {
if (context.persistentStoreCoordinator == self.persistentStoreCoordinator) {
[context mergeChangesFromContextDidSaveNotification:notification];
}
}
}

Cannot update object that was never inserted

I create an category object and save it:
NSManagedObjectContext *managedObjectContext = [[FTAppDelegate sharedAppDelegate] managedObjectContext];
_category = (Category *)[NSEntityDescription
insertNewObjectForEntityForName:#"Category"
inManagedObjectContext:managedObjectContext];
NSError *error = nil;
[managedObjectContext save:&error];
if (error) {
NSLog(#"error saving: %#",error);
}
then edit the name of the category object and save again.
_category.name = _nameTextField.text;
NSManagedObjectContext *managedObjectContext = [[FTAppDelegate sharedAppDelegate] managedObjectContext];
NSError *error = nil;
[managedObjectContext save:&error];
if (error) {
NSLog(#"error saving: %#",error);
}
and get this error:
2013-01-12 17:53:11.862 instacat[7000:907] Unresolved error Error Domain=NSCocoaErrorDomain Code=134030 "The operation couldn’t be completed. (Cocoa error 134030.)" UserInfo=0x2027b300 {NSAffectedObjectsErrorKey=(
"<Category: 0x1ed43cf0> (entity: Category; id: 0x1ed52970 <x-coredata://68E5D7B6-D461-4962-BC07-855349DB3263-7000-00000141BAB4C399/Category/tE8AB2F2E-C14C-4E93-8343-CC245B7726622> ; data: {\n categoryId = nil;\n isPrivate = 0;\n name = techies;\n users = (\n );\n})"
), NSUnderlyingException=Cannot update object that was never inserted.}, {
NSAffectedObjectsErrorKey = (
"<Category: 0x1ed43cf0> (entity: Category; id: 0x1ed52970 <x-coredata://68E5D7B6-D461-4962-BC07-855349DB3263-7000-00000141BAB4C399/Category/tE8AB2F2E-C14C-4E93-8343-CC245B7726622> ; data: {\n categoryId = nil;\n isPrivate = 0;\n name = techies;\n users = (\n );\n})"
);
NSUnderlyingException = "Cannot update object that was never inserted.";
}
Thank you for your time and consideration.
I am using the AFIncrementalStore.
How about something like this:
self.category.name = self.nameTextField.text;
NSManagedObjectContext *managedObjectContext = [[FTAppDelegate sharedAppDelegate] managedObjectContext];
if(![self.category isInserted])
{
[managedObjectContext insertObject:self.category];
}
NSError *error = nil;
[managedObjectContext save:&error];
if (error) {
NSLog(#"error saving: %#",error);
}
Basically check the object is it has been inserted before, if not, insert it and then save the context.
When you update an object, you can't use insertNewObjectForEntityForName, you need to first save your object, then call something like
[self.managedObjectContext refreshObject:_category mergeChanges:YES]
Then use managedObjectContext save again.
This is the difference in direct SQL as "INSERT" and "UPDATE".
Your object is loosing the managedObjectContext. Either use
self.managedObjectContext
or refetch the object in
[[FTAppDelegate sharedAppDelegate] managedObjectContext]
and edit the refetched object and then save it.
I have the same error but different and rare scenario, it happens once in almost 100 attempts. Find my problem below:
I have 2 NSManagedObjects in core data model:
1- Lead
2- LeadAttirbute
Lead has 1-M relationship with LeadAttribute.
There is a form that inputs for lead and refresh(create new lead) the form after submitting a lead. If i keep on submitting the leads then at a stage, [managedObjectContext save:&error]; starts giving below error:
Domain=NSCocoaErrorDomain Code=134030 "The operation couldn’t be completed. (Cocoa error 134030.)" UserInfo=0x1f251740 {NSAffectedObjectsErrorKey=(
" (entity: LeadInfoAttribute; id: 0x1f2eb920 ; data: {\n attributeId = 0;\n lead = nil;\n optional = nil;\n orderId = 0;\n title = nil;\n value = Bjjbjp;\n})"
), NSUnderlyingException=Cannot update object that was never inserted.}
And it keeps on giving the same error until i dont terminate and re-launch the app. I'm not able to update anything in core data model after this error occur, So my questions are:
1- Can we remove the fault state of core data? i.e to capture and delete the object that is creating trouble before making the save call again.
2- What could be the possible reasons for this issue? Since its very rare and can't reproduce this everytime.
I've just run into this issue and in my case the problem was following:
1) create new managed object in context A
2) save the context A
3) retrieve this object by objectID from context B
4) make changes on managed object and save the context B
Normally it wouldn't be a problem, but in this case the context A is child context and therefore doesn't save to persistent store (just to parent context, which isn't the context B). So when fetch for managed object is done from context B, context doesn't have this object. When changes are made, context tries to save them anyway...and thats when this error occurs. In some cases (as #Trausti Thor mentioned) the refreshObject:mergeChanges: method could help (it passes the data to another context).
In Your case I'll check if:
1) managed object context from [[FTAppDelegate sharedAppDelegate] managedObjectContext] returns always the same context
2) when you save the category, check if it was really saved to persistent store (self.category.objectID.isTemporaryID == NO)
NOTE:
The second point is more important, because if you look carefully, your category object still has temporary objectID, that means it's not persisted.
What I think it's happening is that you are not getting the right NSManagedObjectContext.
Think about it as a session. So when you update you are not getting the right session and so your object doesn't exist there.
Before doing the second save try to find your object on that NSManagedObjectContext.
If you need further help please describe what happens between the creation and the update.
Getting the wrong NSManagedObjectContext can be due to bad code on the AppDelegate or accessing from another thread other than the main thread.

Resources