NSUndoManager undo not working with Core Data on iOS - objective-c

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.

Related

Fixing delay in Core Data Storage

So I am building in a hide function into my application. In my settings menu I have a UISwitch that should allow the user to hide themselves. I have created the UISwitch's IBAction like so:
-(IBAction)hideUserToggle:(id)sender {
AppDelegate *newAppDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [newAppDelegate managedObjectContext];
NSManagedObject *newOwner;
NSEntityDescription *entityDesc = [NSEntityDescription entityForName:#"LoggedInUser" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDesc];
NSManagedObject *matches = nil;
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
newOwner = [NSEntityDescription insertNewObjectForEntityForName:#"LoggedInUser" inManagedObjectContext:context];
if (_hideUser.on) {
if ([objects count] == 0) {
NSLog(#"%#",[error localizedDescription]);
} else {
matches = objects[0];
[newOwner setValue:#"userHidden" forKeyPath:#"isHidden"];
NSLog(#"%#",[matches valueForKeyPath:#"isHidden"]);
}
} else {
if([objects count] == 0) {
NSLog(#"%#",[error localizedDescription]);
} else {
matches = objects[0];
[newOwner setValue:#"userNotHidden" forKeyPath:#"isHidden"];
NSLog(#"%#",[matches valueForKeyPath:#"isHidden"]);
}
}
}
This should set the value of the Core Data String that I use to determine whether a person is hidden or not, which I use later in my code as a conditional for loading data. However when I test this feature it doesn't seem to update the persistent data store (Core Data) when the user has flipped the switch. I have looked around everywhere and I found a reference to there being a delay in updating Core Data here -> Why does IOS delay when saving core data via a UIManagedDocument, however it doesn't seem to provide the answer to my problem.
I want to be able flip the switch and save that value so that when the user swipes over to another view controller it is immediately aware that the user has gone into "hiding" or offline so it does not show certain information.
A NSManagedObjectContext is a scratchpad. Changes you make within the context exist only within the context unless or until you save them to the context's parent (either the persistent store itself or another context).
You're not saving them. I'd assume you're therefore not seeing the change elsewhere because you're using different contexts. Meanwhile the change eventually migrates because somebody else happens to save.
See -save: for details on saving.
(aside: the key-value coding [newOwner setValue:#"userHidden" forKeyPath:#"isHidden"]-style mechanism is both uglier and less efficient than using an editor-generated managed object subclass; hopefully it's just there while you're debugging?)

How does the save method in Core Data works?

I'm following a guid in core data, and they implement an action method to preform saving to the database using a ManagedObject. I understand all the code in the method except the method which they say preform the saving, and to me it looks like the method checks if there is an error, and if yes so there is an NSLog to print that there was an error. this is the method:
- (IBAction)save:(id)sender {
NSManagedObjectContext *context = [self managedObjectContext];
// creating a new managed object
NSManagedObject *newDevice = [NSEntityDescription insertNewObjectForEntityForName:#"Device" inManagedObjectContext:context];
[newDevice setValue:self.nameTextField.text forKey:#"name"];
[newDevice setValue:self.versionTextField.text forKey:#"version"];
[newDevice setValue:self.companyTextField.text forKey:#"company"];
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
Obviously something happens in [context save:&error] this call which I'd love if you can explain what?
Calling save: persists changes made to the object graph on the specific context and takes it one level above.
Each context contains its own changeset, and when you call save:, the changes are either taken one level above (to its parent context), or, if there is no parent context, to the store coordinator to be persisted by the method specified when opening the coordinator (SQLite, XML, binary, etc.).
Changes can be modifications, insertions or deletions.
Before saving, changes to objects are verified and objects are notified about the save process.
After saving, notifications are sent to the system to let know various components (such as fetch results controllers, your code, etc.) that the save operation has taken place.

Problem with NSFetchedResultsController updates and a to-one relationship

I'm having some trouble with inserts using a NSFetchedResultsController with a simple to-one relationship. When I create a new Source object, which has a to-one relationship to a Target object, it seems to call - [(void)controller:(NSFetchedResultsController *)controller didChangeObject ... ] twice, with both NSFetchedResultsChangeInsert and NSFetchedResultsChangeUpdate types, which causes the tableview to display inaccurate data right after the update.
I can recreate this with a simple example based off the standard template project that XCode generates in a navigation-based CoreData app. The template creates a Event entity with a timeStamp attribute. I want to add a new entity "Tag" to this event which is just a 1-to-1 relation with Entity, the idea being that each Event has a particular Tag from some list of tags. I create the relationship from Event to Tag in the Core Data editor, and an inverse relationship from Tag to Event. I then generate the NSManagedObject sub-classes for both Event and Tag, which are pretty standard:
#interface Event : NSManagedObject {
#private
}
#property (nonatomic, retain) NSDate * timeStamp;
#property (nonatomic, retain) Tag * tag;
and
#interface Tag : NSManagedObject {
#private
}
#property (nonatomic, retain) NSString * tagName;
#property (nonatomic, retain) NSManagedObject * event;
I then pre-filled the Tags entity with some data at launch, so that we can pick from a Tag when inserting a new Event. In AppDelegate, call this before returning persistentStoreCoordinator:
NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Tag" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
//check if Tags haven't already been created. If not, then create them
if (fetchedObjects.count == 0) {
NSLog(#"create new objects for Tag");
Tag *newManagedObject1 = [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:context];
newManagedObject1.tagName = #"Home";
Tag *newManagedObject2 = [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:context];
newManagedObject2.tagName = #"Office";
Tag *newManagedObject3 = [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:context];
newManagedObject3.tagName = #"Shop";
}
[fetchRequest release];
if (![context save:&error])
{
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
Now, I changed the insertNewObject code to add a Tag to the Event attribute we're inserting. I just pick the first one from the list of fetchedObjects for this example:
- (void)insertNewObject
{
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
Event *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
// If appropriate, configure the new managed object.
// Normally you should use accessor methods, but using KVC here avoids the need to add a custom class to the template.
[newManagedObject setValue:[NSDate date] forKey:#"timeStamp"];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entityTag = [NSEntityDescription entityForName:#"Tag" inManagedObjectContext:context];
[fetchRequest setEntity:entityTag];
NSError *errorTag = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&errorTag];
if (fetchedObjects.count > 0) {
Tag *newtag = [fetchedObjects objectAtIndex:0];
newManagedObject.tag = newtag;
}
// Save the context.
NSError *error = nil;
if (![context save:&error])
{
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
I want to now see the tableview reflecting these changes, so I made the UITableViewCell to type UITableViewCellStyleSubtitle and changed configureCell to show me the tagName in the detail text label:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
Event *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [[managedObject valueForKey:#"timeStamp"] description];
cell.detailTextLabel.text = managedObject.tag.tagName;
}
Now everything's in place. When I call insertNewObject, it seems to create the first row fine, but the 2nd row is a duplicate of the first, even though the timestamp should be a few seconds apart:
When I scroll the screen up and down, it refreshes the rows and then displays the right results with the correct time. When I step through the code, the core problem comes up: inserting a new row seems to be calling [(NSFetchedResultsController *)controller didChangeObject ...] twice, once for the insert and once for an update. I'm not sure WHY the update is called though. And here's the clincher: if I remove the inverse relationship between Event and Tag, the inserts starts working just fine! Only the insert is called, the row isn't duplicated, and things work well.
So what is it with the inverse relationship that is causing NSFetchedResultsController delegate methods to be called twice? And should I just live without them in this case? I know that XCode gives a warning if the inverse isn't specified, and it seems like a bad idea. Am I doing something wrong here? Is this some known issue with a known work-around?
Thanks.
With regards to didChangeObject being called multiple times, I found one reason why this will going to happen. If you have multiple NSFetchedResultsController in your controller that shares NSManagedObjectContext, the didChangeObject will be called multiple times when something changes with the data. I stumbled on this same issue and after a series of testing, this was the behavior I noticed. I have not tested though if this behavior will going to happen if the NSFetchedResultsControllers does not share NSManagedObjectContext. Unfortunately, the didChangeObject does not tell which NSFetchedResultsController triggered the update. To achieve my goal, I ended up using a flag in my code.
Hope this helps!
You can use [tableView reloadRowsAtIndexPaths:withRowAnimation:] for NSFetchedResultsChangeUpdate instead of configureCell method.
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
I had the same problem. And have a solution.
Under certain circumstances, the NSFetchedResultsController gets fired twice when calling the -(BOOL)save: on the managed object context, directly after inserting or manipulating.
In my case, I'm doing some magic with the object in the NSManagedObject -(void)willSave method, which causes the NSFetchedResultsController to fire twice. This seems to be a bug.
Not to manipulate inserted objects while being saved did the trick for me!
To delay the context save to a later run loop seems to be another solution, for example:
dispatch_async(dispatch_get_main_queue(), ^{ [context save:nil]; });
Objects in NSFetchedResultsController must be inserted with permanent objectID. After creating object and before saving to persistent store, it has temporary objectID. After saving object receive permanent objectID. If object with temporary objectID is inserted into NSFetchedResultsController, then after save object and change its objectID to permanent, NSFetchedResults controller may report about inserting fake duplicate object.
Solution after instantiating object that will be fetched in NSFetchedResultsController - just call obtainPermanentIDsForObjects on its managedObjectContext with it.

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];
...
}