Confused about how fetchResultController works with MagicalRecord - objective-c

I have a NSOperation subclass,this is the main method:
(void)main
{
NSAutoreleasePool *Pool = [[NSAutoreleasePool alloc] init];
managedObjectContext = [NSManagedObjectContext contextThatNotifiesDefaultContextOnMainThread];
Message *message = (Message *) [managedObjectContext objectWithID:self.messageID];
message.status = [NSNumber numberWithInt:SKMessageSendStateStart];
[message save];
[self send];
[self finish];
[Pool drain];
}
I define the fetchResultController and defaultContext like this:
(BOOL)application:(UIApplication *)applicationdidFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[MagicalRecordHelpers setupCoreDataStackWithStoreNamed:#"Shark"];
self.context = [NSManagedObjectContext context];
[NSManagedObjectContext setDefaultContext:self.context];
self.fetchController = [Message fetchRequestAllGroupedBy:nil withPredicate:nil sortedBy:#"text" ascending:YES];
[self.fetchController setDelegate:self];
[self.fetchController performFetch:nil];
}
Everytime i call [message save],the console logout:
-NSManagedObjectContext(MagicalRecord) mergeChangesFromNotification: Merging changes to * DEFAULT
context on Main Thread *
But the NSFetchedResultsControllerDelegate never get called!
Does this mean that I set the FetchedResultsController wrong or what? I
am totally confused.
Thanks in advance.

The reason this isn't working is because MagicalRecord will automatically call performFetch: for you, thus not allowing you to set the delegate ahead of time.
Also, in your applicationDidFinishLaunching: method, you want to delete these lines:
self.context = [NSManagedObjectContext context];
[NSManagedObjectContext setDefaultContext:self.context];
You want to NOT change the default context in this case. MagicalRecord is handling stuff for you when you call setupCoreDataStackWithStoreNamed:...that is, a MOC is already available for use when that method completes, there is no need to toss the one it created for you and set the default context to a new instance in this particular case.
It's also not necessary to hold on to the context if all you're doing is going to use it to pass to one of the fetch methods provided by MagicalRecord. MagicalRecord will create a single context for its use (the 'default' context) and just use that...

I misunderstood what the [NSManagedObjectContext context] means. It create a new context in the main thread. Since the context that the fetchResultController monitor is not the same context that the change merged to, the NSFetchedResultsControllerDelegate will not get called.
Change [NSManagedObjectContext context] to [NSManagedObjectContext defaultContext] solved the problem.

Related

NSManagedObjectContext and performBlock, changes on iOS 8?

I have an old project (started on iOS 7) with this simple code:
in AppDelegate I create the managedObjectContext:
- (NSManagedObjectContext *)managedObjectContext {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
return _managedObjectContext;
}
Then, I perform an update operation in a view controller:
[context performBlock:^{
[context deleteObject:anObject];
NSError *error = nil;
if (![context save:&error])
{
NSLog(#"Error saving context");
}
}];
I'm sure that this code was working correctly on iOS 7.0, but crashes on the performBlock: call on iOS 8 with this error:
Can only use -performBlockAndWait: on an NSManagedObjectContext that was created with a queue.
I can solve the error changing the init method of the NSManagedObjectContext instance like this:
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
But I don't understand why this behavior changed. In the documentation of NSManagedObjectContext you can read:
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.
So, in my first example, using a simple init call, the queue owner of the context is the main thread.
The performBlock: call was made on the main thread too, so I don't understand why this error. Am I missing something or it's a bug in iOS 8?
The call -[NSManagedObjectContext init] is just a wrapper for -[NSManagedObjectContext initWithConcurrencyType:] with the argument NSConfinementConcurrencyType. This creates an instance of NSManagedObjectContext that uses the obsolete thread confinement model - which does not uses a queue. Contexts created using init or initWithConcurrencyType: with the value NSConfinementConcurrencyType passed are not compatible with the queue methods performBlock: or performBlockAndWait:.
Create your context using -[NSManagedObjectContext initWithConcurrencyType:] and pass either NSPrivateQueueConcurrencyType or NSMainQueueConcurrencyType as appropriate. The context that is created as a result is compatible with performBlock: and performBlockAndWait: and will uses the queue confinement model that was introduced in iOS 5.

NSUndoManager undo not working with Core Data on iOS

I have a problem with NSUndoManager not undoing when working with a complex model.
This is my model.
I have a Singleton that takes care of the core data stuff, this is its initialization:
model =[NSManagedObjectModel mergedModelFromBundles:nil];
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSString *path = pathInDocumentDirectory(#"store.data");
NSURL *storeURL = [NSURL fileURLWithPath:path];
NSError *error = nil;
if(![psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error]) {
[NSException raise:#"Open failed" format:#"Reason: %#", [error localizedDescription]];
}
context = [[NSManagedObjectContext alloc] init];
NSUndoManager *contextUndoManager = [[NSUndoManager alloc] init];
[contextUndoManager setLevelsOfUndo:20];
[context setUndoManager:contextUndoManager];
[context setPersistentStoreCoordinator:psc];
For every entity I have an init method that calls [self initWithEntity...] and then inits some properties. This is an example of the HalfEdge entity:
- (id) initWithVertex:(Vertex*) vert inManagedObjectContext: context {
NSEntityDescription* tEntityDescription = [NSEntityDescription entityForName: #"HalfEdge"
inManagedObjectContext: context];
self = [self initWithEntity: tEntityDescription insertIntoManagedObjectContext: context];
if(self) {
self.lastVertex = vert;
[self.lastVertex addHalfEdgeObject:self];
}
return self;
}
When the user adds a new drawing I create a new Drawing entity and then let the user add points taping the screen. For every point a rutine will get executed that may add and/or remove Triangle entities, halfedges and vertex. This is the call:
[[[DrawingsStore sharedStore].managedObjectContext undoManager] beginUndoGrouping];
[delaunay addPoint:CGPointMake(localizacion.x-dummy.bounds.size.width/2, localizacion.y-dummy.bounds.size.height/2)];
[[DrawingsStore sharedStore].managedObjectContext processPendingChanges];
[[[DrawingsStore sharedStore].managedObjectContext undoManager] endUndoGrouping];
As you can see, I set an undo group for everything that happens inside that rutine.
Then when a button is pressed I'm calling [[context undoManager] undo]; but it does nothing.
I print a fetch before and after the undo and it's the same. I can see the rutine is working properly, adding all the correct entities to core data, but then it wont undo anything at all.
EDIT with sugestions from Aderstedt
Ok, I deleted the custom init method for NSManagedObject subclasses and created a class method like this:
+ (HalfEdge*) addWithVertex:(Vertex*) vert inManagedObjectContext: context {
HalfEdge* halfEdge = [NSEntityDescription insertNewObjectForEntityForName:#"HalfEdge" inManagedObjectContext:context];
if(halfEdge) {
halfEdge.lastVertex = vert;
[halfEdge.lastVertex addHalfEdgeObject:self];
}
return halfEdge;
}
And still the same result. Objects get created, undo doesn't work. (canUndo returns 1)
EDIT
Wow, I just registered for NSUndoManagerCheckpointNotification of undoManager and once I click undo it gets posted forever like in a loop. Ok, now I know I must be doing something wrong somewhere, but... where?
Ok I found out. Turns out I was looking in the wrong place.
Trying to Debug NSUndoManager I registered for notifications and found out that NSUndoManagerCheckpointNotification was getting called over and over again.
[delaunay addPoint...] makes all the changes to the model. But at the same time there is a render routine running that renders the triangles to the screen. In that routine I set the color of those triangles. I need to do it there because I don't know the color I should put before I render the background of the screen.
Those changes to the color attribute of the NSManagedObject subclass Triangle were causing the NSUndoManagerCheckpointNotification to be fired and the undo to not work. If I remove that, undo works.
So I figured I just have to add this, so the changes made during render don't make it to the undo stack.
[[[DibujosStore sharedStore] managedObjectContext] processPendingChanges];
[[[[DibujosStore sharedStore] managedObjectContext] undoManager] disableUndoRegistration];
[renderer render];
[[[DibujosStore sharedStore] managedObjectContext] processPendingChanges];
[[[[DibujosStore sharedStore] managedObjectContext] undoManager] enableUndoRegistration];
You're creating NSManagedObject instances The Wrong Way™. Use
- [NSEntityDescription insertNewObjectForEntityForName:... inManagedObjectContext...]
to insert new objects. If you want to do custom processing for the object when it is inserted, override
- (void)awakeFromInsert
in your NSManagedObject subclass. Please check the Core Data documentation, it explicitly states that you are discouraged from overriding initWithEntity.... Now, as for your undo problem, your call to
[delaunay addPoint:CGPointMake(localizacion.x-dummy.bounds.size.width/2, localizacion.y-dummy.bounds.size.height/2)];
... does that actually change any attributes on Core Data objects? Other instance variables, cached arrays et.c. will not be automatically registered for undo. If you do change attributes on Core Data objects, please check to see that [context undoManager] isn't nil.

NSFetchedResultsController not firing delegate method after merging update from background thread

I have an NSFetchedResultsController and a few operations that inserts and updates managed objects on separate threads via NSOperationQueue.
The FRC looks like this, note that I have set the cache to nil:
[NSFetchedResultsController deleteCacheWithName:nil];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
Each threaded operation has its own managed object context and fires mergeChangesFromContextDidSaveNotification to the main MOC each time it saves changes.
The code that I use to merge contexts looks like this:
- (void)mergeContextChanges:(NSNotification *)notification
{
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
if([NSThread isMainThread] == NO)
{
[self performSelectorOnMainThread:_cmd withObject:notification waitUntilDone:NO];
return;
}
NSSet *updated = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
for(NSManagedObject *thing in updated)
{
NSLog(#"Background thread updated %#", [thing description]);
}
for(NSManagedObject *thing in updated)
{
[[context objectWithID:[thing objectID]] willAccessValueForKey:nil];
}
[context mergeChangesFromContextDidSaveNotification:notification];
}
I can confirm by looking at the logs that each time the background operations insert or update data, my mergeContextChanges: method is being called with the proper insert/update values.
The problem is that while merging inserts are firing the FRCs delegate methods (ex. controllerDidChangeContent:) properly, merging updates doesn't signal the FRC to fire its delegate methods.
Strange enough, I can also confirm that the FRC fires its delegates properly if I run the updates on the main thread using the main MOC.
How can I make the FRC fire its delegate methods when updated MOCs are merged?
More Info: Looks like using any MOC other than the main MOC and trying to merge updates to the main MOC has the same results; the FRC refuses to notice it.
Oh..... man.
It looks like accessing the main MOC from within my threaded operations (even if it's for a task unrelated to the data I was trying to update) causes this weird behavior.
I hope this helps anyone else that runs into this problem.

How to correctly handle threading when drawing Core Data entity information with CATiledLayer

I'm contemplating how to offload the drawing of a very large Core Data tree structure to CATiledLayer. CATiledLayer seems to be awesome because it performs drawing on a background thread and then fades in tiles whenever they're drawn. However, because the information of the drawing comes from a Core Data context that is by design not thread safe, I'm running into race condition issues where the drawing code needs to access the CD context.
Normally, if I need to perform background tasks with Core Data, I create a new context in the background thread and reuse the existing model and persistent store coordinator, to prevent threading issues. But the CATiledLayer does all the threading internally, so I don't know when to create the context, and there needs to be some kind of context sharing, or I can't pass the right entities to the CATiledLayer to begin with.
Is there anyone with a suggestion how I can deal with this scenario?
Cheers,
Eric-Paul.
The easiest solution is to use the dispatch API to lock all of your data access onto a single thread, while still allowing the actual drawing to be multi-threaded.
If your existing managed object context can only be accessed on the main thread, then this is what you do:
- (void)drawInContext:(CGContextRef)context // I'm using a CATiledLayer subclass. You might be using a layer delegate instead
{
// fetch data from main thread
__block NSString *foo;
__block NSString *bar;
dispatch_sync(dispatch_get_main_queue(), ^{
NSManagedObject *record = self.managedObjecToDraw;
foo = record.foo;
bar = record.bar;
});
// do drawing here
}
This is a quick and easy solution, but it will lock your main thread while fetching the data, which is almost certainly going to create "hitches" whenever a new tile is loaded while scrolling around. To solve this, you need to perform all of your data access on a "serial" dispatch queue.
The queue needs to have it's own managed object context, and you need to keep this context in sync with the context on your main thread, which is (presumably) being updated by user actions. The easiest way to do this is to observe a notification that the context has changed, and throw out the one used for drawing.
Define an instance variable for the queue:
#interface MyClass
{
NSManagedObjectContext *layerDataAccessContext;
dispatch_queue_t layerDataAccessQueue;
}
#end
Create it in your init method:
- (id)init
{
layerDataAccessQueue = dispatch_queue_create("layer data access queue", DISPATCH_QUEUE_SERIAL);
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextDidChange:) name:NSManagedObjectContextDidSaveNotification object:nil]; // you might want to create your own notification here, which is only sent when data that's actually being drawn has changed
}
- (void)contextDidChange:(NSNotification *)notif
{
dispatch_sync(layerDataAccessQueue, ^{
[layerDataAccessContext release];
layerDataAccessContext = nil;
});
[self.layer setNeedsDisplay];
}
And access the context while drawing:
- (void)drawInContext:(CGContextRef)context
{
// fetch data from main thread
__block NSString *foo;
__block NSString *bar;
dispatch_sync(layerDataAccessQueue, ^{
NSManagedObject record = self.managedObjectToDraw;
foo = record.foo;
bar = record.bar;
});
// do drawing here
}
- (NSManagedObject *)managedObjectToDraw
{
if (!layerDataAccessContext) {
__block NSPersistentStoreCoordinator *coordinator;
dispatch_sync(dispatch_get_main_queue(), ^{
coordinator = [self persistentStoreCoordinator];
});
layerDataAccessContext = [[NSManagedObjectContext alloc] init];
[layerDataAccessContext setPersistentStoreCoordinator:coordinator];
}
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"Employee"
inManagedObjectContext:layerDataAccessContext];
[request setEntity:entity];
NSPredicate *predicate =
[NSPredicate predicateWithFormat:#"self == %#", targetObject];
[request setPredicate:predicate];
NSError *error = nil;
NSArray *array = [layerDataAccessContext executeFetchRequest:request error:&error];
NSManagedObject *record;
if (array == nil || array.count == 0) {
// Deal with error.
}
return [array objectAtIndex:0];
}
I've given up trying to share managed object context instances between CATiledLayer draws and now just alloc/init a new context at every call of drawLayer:inContext: The performance hit is not noticable, for the drawing is already asynchronous.
If there's anyone out there with a better solution, please share!

How do you populate a NSArrayController with CoreData rows programmatically?

After several hours/days of searching and diving into example projects I've concluded that I need to just ask. If I bind the assetsView (IKImageBrowserView) directly to an IB instance of NSArrayController everything works just fine.
- (void) awakeFromNib
{
library = [[NSArrayController alloc] init];
[library setManagedObjectContext:[[NSApp delegate] managedObjectContext]];
[library setEntityName:#"Asset"];
NSLog(#"%#", [library arrangedObjects]);
NSLog(#"%#", [library content]);
[assetsView setDataSource:library];
[assetsView reloadData];
}
Both NSLogs are empty. I know I'm missing something... I just don't know what. The goal is to eventually allow multiple instances of this view's "library" filtered programmatically with a predicate. For now I'm just trying to have it display all of the rows for the "Asset" entity.
Addition: If I create the NSArrayController in IB and then try to log [library arrangedObjects] or manually set the data source for assetsView I get the same empty results. Like I said earlier, if I bind library.arrangedObjects to assetsView.content (IKImageBrowserView) in IB - with same managed object context and same entity name set by IB - everything works as expected.
- (void) awakeFromNib
{
// library = [[NSArrayController alloc] init];
// [library setManagedObjectContext:[[NSApp delegate] managedObjectContext]];
// [library setEntityName:#"Asset"];
NSLog(#"%#", [library arrangedObjects]);
NSLog(#"%#", [library content]);
[assetsView setDataSource:library];
[assetsView reloadData];
}
I was running into a similar situation where the (IKImageBrowserView) was not initializing even though the ArrayController would ultimately sync up with the NSManagedObjectContext.
Ultimately found this passage in the core data programming guide
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Articles/cdBindings.html#//apple_ref/doc/uid/TP40004194-SW3
if the "automatically prepares content" flag (see, for example,
setAutomaticallyPreparesContent:) is set for a controller, the controller's initial content
is fetched from its managed object context using the controller's current fetch predicate. It
is important to note that the controller's fetch is executed as a delayed operation performed
after its managed object context is set (by nib loading)—this therefore happens after
awakeFromNib and windowControllerDidLoadNib:. This can create a problem if you want to
perform an operation with the contents of an object controller in either of these methods,
since the controller's content is nil. You can work around this by executing the fetch
"manually" with fetchWithRequest:merge:error:.
- (void)windowControllerDidLoadNib:(NSWindowController *) windowController
{
[super windowControllerDidLoadNib:windowController];
NSError *error = nil;
BOOL ok = [arrayController fetchWithRequest:nil merge:NO error:&error];
// ...
It looks like the problem is that you have not actually told the NSArrayController to fetch anything. NSArrayControllers are empty until you add objects either through bindings or manually.
After setting up library try to call its fetch method:
[library fetch:self];
Also, you probably know this already but it is possible to set bindings in code with the following method:
- (void)bind:(NSString *)binding toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
Can also be added in awakeFromNib if subclassing the NSArrayCotroller or via your view controller
-(void)awakeFromNib
{
[self fetchWithRequest:nil merge:NO error:nil];
...
}