Disable undo for creation/deletion of NSManagedObject - objective-c

In my Core Data model, I've got a relationship called listItems which links to several listItem entities, each with a stringValue attribute. I've created a control which is essentially a list of NSTextFields, one for each list item. The control is bound to listItems properly, and I've set it up so that pressing the return key creates a new field directly under the currently-edited one and changes the focus to the new field. So, essentially, to add a new item, the user presses Return.
Likewise, if the user ends editing and the currently-edited field is empty, the field is removed (as in, empty fields only appear during "edit mode", so to speak). This works pretty well. Basically, in my listItem NSManagedObject subclass, I do the following:
// Don't allow nil values
if (!value && [[self.recipe ingredients] count] > 1) {
for (EAIngredientRef *ingredient in [self.recipe ingredients]) {
if ([[ingredient sortIndex] integerValue] > [[self sortIndex] integerValue]) {
[ingredient setSortIndex:[NSNumber numberWithInteger:([[ingredient sortIndex] integerValue]-1)]];
}
}
[[self managedObjectContext] deleteObject:self];
return;
}
// Code to handle if it is a real value
The problem I am encountering is that each time a row is deleted this way, it registers with the undoManager. Thus, if I edit a row, press Return (which creates a new row), and click away to end editing, the row disappears. However, if I then undo, the empty field reappears. My goal is to have delete operations involving empty fields be ignored by the undoManager.
How would I go about this? I've tried using [[[self managedObjectContext] undoManager] disableUndoRegistration] and the associated enableUndoRegistration in several spots (such as -didTurnIntoFault, but I suspect that the undo registration might be happening prior to that method)

If you dive more deeply into the Core Data docs, you'll find this tidbit hidden away:
[[self managedObjectContext] processPendingChanges];
[[[self managedObjectContext] undoManager] disableUndoRegistration];
// Do your work
[[self managedObjectContext] processPendingChanges];
[[[self managedObjectContext] undoManager] enableUndoRegistration];
Changes are not registered with the undo manager normally until the end of the event loop, and so were being registered after you'd turned undo registration back on. The above forces it to occur when you want.

Related

UICollectionViewLayout supplementary view duplicate on delete

I have been tinkering with a UICollectionViewLayout for a few days now, and seem to be stuck on getting a Supplementary view to delete with animation using the finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
I have finally made some headway and discovered this issue. Initially it looked as if it was not animating, because i was not getting a fading animation. so i added a transform on it to get it to rotate... and discovered a supplementary view is in fact being animated, but another one just sits there. until its finished and the unceremoniously disappears not being affected by being told to disappear at all. So to sum up it looks like an exact copy is made and that is animated out for finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath: and the original just sits there unmoving and animating, then both disappear.
This copy or whatever it is does not disappear either until i scroll the view.
Does anyone have any insight into this issue?
Update
So after some tweaking, experimentation and many hypothesis later as to what was causing the issue, it turns out that the culprit was this method
- (NSArray *)indexPathsToDeleteForSupplementaryViewOfKind:(NSString *)kind
{
NSMutableArray *deletedIndexPaths = [NSMutableArray new];
[self.updatedItems enumerateObjectsUsingBlock:^(UICollectionViewUpdateItem *updateItem, NSUInteger idx, BOOL * _Nonnull stop)
{
switch (updateItem.updateAction)
{
case UICollectionUpdateActionInsert:
{
if (_unReadHeaderIndexPath != nil && [self.layoutInformation[ChatroomLayoutElementKindUnreadHeader] count] == 0)
{
[deletedIndexPaths addObject:_unReadHeaderIndexPath];
_unReadHeaderIndexPath = nil;
}
}
break;
case UICollectionUpdateActionDelete:
{
[deletedIndexPaths addObject:updateItem.indexPathBeforeUpdate];
}
break;
default:
break;
}
}];
return deletedIndexPaths;
}
The part to focus on is here
if (_unReadHeaderIndexPath != nil && [self.layoutInformation[ChatroomLayoutElementKindUnreadHeader] count] == 0)
{
[deletedIndexPaths addObject:_unReadHeaderIndexPath];
_unReadHeaderIndexPath = nil;
}
Particularly the _unReadHeaderIndexPath = nil;
What this is doing is making a supplementary view disappear which indicates unread messages when the user inserts any text and then setting it to nil so it is not added multiple times, the logic is sound however the method in which is employed seems to be the issue.
indexPathsToDeleteForSupplementaryViewOfKind: is called multiple times and one can assume it needs multiple passes through it to work properly but since we nil _unReadHeaderIndexPath; it seems like it isn't added to the returned array at the appropriate time.
It also seems like the incorrect method in as I'm checking for insertion action in a method name indexPathsToDeleteForSupplementaryViewOfKind: so the solution was to move all of that logic to the method
prepareForCollectionViewUpdates:
Where we originally set the values for self.updatedItems in the first place. Which means i can get rid of that member variable altogether. And it animates and disappears without any odd copy sticking around.

ArrayController deleting from tableview but not CoreData

I have a pretty simple setup.
I have a NSTableView within my MainWindow.xib, whose value is bound to an ArrayController like so:
The ArrayController is setup like this:
I have a TestModel.xcdatamodeld which contains one entity, Test, with one attribute, a body with type of string.
I then have my window set up simply like this, with a textfield and two buttons to add and remove from the array controller:
When I add or remove any entry into the tableview, it works fine. But when I close and restart my app, those changes aren't synced to the core data.
What am I doing wrong?
Thanks
Edit: I'm using Magical Record, too.
Did you save changes with the context?
Something like:
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}

Setting NSMatrix selection programmatically causing mutex lock when trying to later get selected cell

Overview
I'm seeing a strange issue in a Mac OS X Cocoa app I'm developing w/Xcode 4.3.2 and testing on Mac OS X 10.7.5 (targeting Mac OS X 10.6): I have a basic NSMatrix radio group in my main NIB that has one outlet and one action in my main controller, but is also used in two other methods (also in the main controller). The action saves the selected tag to NSUserDefaults & enables/disables an NSTextField based on which is selected, one method is a preferences loader and so selects a cell by tag upon load, and the other method looks up it's selected tag for other logic (code samples below).
Basic stuff, but in one case (when only having selected a cell in the NSMatrix programmatically, not via clicking a radio button in the GUI), the app PWODs in a mutex lock.
The Code & Functionality
I've renamed variables & methods below, but have not changed any of the other structure of the code. passwordRadioGroup is the NSMatrix* IBOutlet.
- (void)applicationDidFinishLaunching:(NSNotification*)aNotification {
// get logging up and running
[self initLogging];
// load various prefs
// ...
[self loadPasswordSettings];
}
- (void)loadPasswordSettings {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// load password settings from our prefs
NSNumber *passwordTypeTag = [defaults objectForKey:kPasswordTypeDefaultKey];
if ( passwordTypeTag != nil ) {
[passwordRadioGroup selectCellWithTag:[passwordTypeTag intValue]];
} else {
[passwordRadioGroup selectCellWithTag:kPasswordTypeRadioRandomTag];
}
[self selectPasswordType:passwordRadioGroup];
// ...
}
- (IBAction)selectPasswordType:(NSMatrix *)sender {
NSInteger tag = [[sender selectedCell] tag];
switch ( tag ) {
case kPasswordTypeRadioRandomTag:
// disable the password text field
[passwordField setEnabled:NO];
break;
case kPasswordTypeRadioManualTag:
// enable the password text field
[passwordField setEnabled:YES];
break;
}
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInt:tag] forKey:kPasswordTypeDefaultKey];
}
- (NSString *)generateUserPassword {
NSString *password;
// which type of password should we be generating (random or from a specifed one)?
NSInteger tag = [[passwordRadioGroup selectedCell] tag];
switch ( tag ) {
// manual password
case kPasswordTypeRadioManualTag:
password = [passwordField stringValue];
break;
// random password
case kPasswordTypeRadioRandomTag:
// password = randomly generated password;
break;
}
return password;
}
When the app is launched, my main controller is sent a applicationDidFinishLaunching message which then sends the loadPasswordSettings message to itself. loadPasswordSettings then sends [[passwordRadioGroup selectedCell] tag] and sends [passwordRadioGroup selectCellWithTag:]. This works correctly and the correct radio button is selected in the GUI. It finally calls [self selectPasswordType:passwordRadioGroup (which also successfully calls [[passwordRadioGroup selectedCell] tag]) and appropriately enables/disables the text field and writes the tag back out to NSUserDefaults (usually, but not always redundant).
You can select any of the radio buttons, which sends a selectPasswordType: message to my main controller (passing it the instance of passwordRadioGroup; I've verified the memory address in the debugger and can inspect its ivars). This successfully calls [[passwordRadioGroup selectedCell] tag] and saves the tag to NSUserDefaults.
You can do the above two as many times as you like without issue. Quitting & relaunching correctly restores the radio buttons to the state you left them last, so it's definitely correctly getting the selected tag, storing it in NSUser defaults, retrieving it from NSUserDefaults, and setting the selected cell by tag on the NSMatrix.
Here's where it gets screwy:
There's another button that, when clicked, does a bunch of other work and ultimately then sends the generateUserPassword message to itself to generate a password (again, this is all in the main controller and running in the main thread). What's the first thing that it does? Calls [[passwordRadioGroup selectedCell] tag]. Fine, you can safely do that as much as you want, as illustrated above, right?
If you select one of the radio buttons, therefore changing the selected cell via the GUI and sending the selectPasswordType: message to my main controller again, yes. You will encounter no issues (although, I admit it seems a bit slow and PWODs for a second).
If you do not click on the NSMatrix after launch (so not forcing the selection/action again from the GUI), that [[passwordRadioGroup selectedCell] tag] call in generateUserPassword will immediately PWOD. If I hit the pause button in Xcode's debugger to see where it's at, it's always in psych_mutexwait (called from class_lookupMethodAndLoadCache) in the main thread. If selectPasswordType: weren't called programmatically and able to run [[passwordRadioGroup selectedCell] tag] without issue, I'd have at least some sanity left.
Help!
To reiterate, I've followed this through in the debugger and can verify that the memory address & ivars confirm that passwordRadioGroup is not being changed out from under me, nor is it being deallocated (I've tried with & without "Zombie Objects" enabled). The only references to passwordRadioGroup in my main controller are those seen above. Googling for all sorts of NSMatrix/radio/selectCellWithTag/selectedCell/selectedTag/class_lookupMethodAndLoadCache/mutex terms/combinations has not been fruitful.
Any solutions, troubleshooting suggestions, or thoughts would be greatly appreciated. Slaps for stupidity also welcome, if deserved.

Core Data UndoManager sends weird NS

The app I'm working on lets users manage some assets. The user can create / delete / edit / split / move assets around on the screen. Users need to be able to undo all these steps back.
The assets are managed with core data (and yes, the undoManager is instantiated).
For each of these actions I create undo groupings with this pair:
beginUndoGrouping ... endUndoGrouping
Here's a simple example (sequence 1):
// SPLIT
- (void) menuSplitPiece: (id) sender
{
[self.managedObjectContext.undoManager beginUndoGrouping];
[self.managedObjectContext.undoManager setActionName:#"Split"];
//... do the split
[self.managedObjectContext.undoManager endUndoGrouping];
// if the user cancels the split action, call [self.managedObjectContext.undoManager undo] here;
}
I do the same for edit: if the user cancels the edit, then I call undo right after endUndoGrouping.
Everything works beautiful with one exception: besides the groups that I create, there are other groups being created by Core Data which I cannot control. Here's what I mean by that:
I registered to receive NSUndoManagerDidCloseUndoGroupNotification notifications like so:
- (void) registerUndoListener
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(didCloseUndoGroup:)
name:NSUndoManagerDidCloseUndoGroupNotification object:nil];
...}
I use these notifications to refresh the Undo button and display the name of the action that is being undone as a result of some action: e.g. Undo Split
Yet, didCloseUndoGroup is called / notified twice for every action above (e.g. section 1, after endUndoGrouping):
At the time of the first notification, self.managedObjectContext.undoManager.undoActionName contains the undo action name I've set, which I expected, while the second time undoActionName is an empty string.
As a workaround, I tried to simply undo operations that had an empty name (Assuming they were not mine and I did not need them), and see whether I was missing anything.
Now, didCloseUndoGroup looks like this
- (void) didCloseUndoGroup: (NSNotification *) notification
{
...
if ([self.managedObjectContext.undoManager.undoActionName isEqualToString:#""]){
[self.managedObjectContext.undoManager undo];
}
[self refreshUndoButton]; // this method displays the name of the undo action on the button
...
}
And magically it works, I can undo any command, any number of layers using "undo". But this is not the way it should work...
Several other things I tried before that:
[self.managedObjectContext processPendingChanges] before opening any grouping. It was still sending two notifications.
Another thing I tried was disableUndoRegistration / enableUndoRegistration. This one generated an exception: "invalid state, undo was called with too many nested undo groups"
None of the above helped me "isolate" the mysterious groupings I mentioned before.
I should not be receiving NSUndoManagerDidCloseUndoGroupNotification notifications twice. Or, should I? Is there a better way to deal with this situation?
UPDATE
This is what finally worked. Previously I was automatically undoing the no-name groups as soon as I received a notification. This is what caused the problem. Now, I undo everything until I reach my target group and then I do a last undo for that group.
"undoManagerHelper" is just a stack management system that generates a unique ID for each command that is pushed on the stack. I use this unique ID to name the group.
- (BOOL) undoLastAction
{
NSString *lastActionID = [self.undoManagerHelper pop]; // the command I'm looking for
if (lastActionID == nil) return false;
//... undo until there is nothing to undo or self.managedObjectContext.undoManager.undoActionName equals lastActionID
//the actual undo here
if ([currentActionID isEqualToString: lastActionID] && [self.managedObjectContext.undoManager canUndo]){
[self.managedObjectContext.undoManager undo];
}
return true;
}
- (void) beginUndoGroupingWithName: (NSString *) name
{
[self.managedObjectContext processPendingChanges];
[self.managedObjectContext.undoManager beginUndoGrouping];
NSString *actionID = [self.undoManagerHelper push: name];
[self.managedObjectContext.undoManager setActionName:actionID];
}
- (void) closeLastUndoGrouping
{
[self.managedObjectContext.undoManager endUndoGrouping];
[self.managedObjectContext processPendingChanges];
}
According to the documentation for beginUndoGrouping - https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/Foundation/Classes/NSUndoManager_Class/Reference/Reference.html - "By default undo groups are begun automatically at the start of the event loop, but you can begin your own undo groups with this method, and nest them within other groups." The unnamed group is the default undo group that contains all operations, it sounds like you should ignore the unnamed group for your situation.

Bindings with NSManagedObject from child context only working for NEW objects

Background:
In my app, I'm specifically targeting Mac OS X Lion. This issue involves Core Data, an NSPopover and a child NSManagedObjectContext (created by using the new parentContext property of NSManagedObjectContext).
I have a table of NSManagedObjects of class "Location". There's an Add button that calls addLocation: and if a table row is double-clicked, I call tableViewDoubleClick:.
For either case, what I do is create a new NSManagedObjectContext and set its parent context to that of the document's context. I then either create a new Location in that context or fetch the Location to be edited from the temporary context. I set the popover's representedObject property to the location in question. If I cancel the popover, nothing is saved. If the user clicks a Save button in the popover, I just call save: on the temporary context and the changes get pushed to the main context.
addLocation:
- (IBAction)addLocation:(id)sender
{
LocationEditViewController *popupController = [[[LocationEditViewController alloc] init] autorelease];
popupController.title = #"Add New Location";
NSManagedObjectContext *tempContext = [[[NSManagedObjectContext alloc] init] autorelease];
tempContext.parentContext = self.document.managedObjectContext;
Location *tempLocation = [NSEntityDescription insertNewObjectForEntityForName:#"Location" inManagedObjectContext:tempContext];
popupController.representedObject = tempLocation;
popupController.managedObjectContext = tempContext;
[popupController.popover showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
}
tableViewDoubleClick:
- (void)tableViewDoubleClick:(id)sender
{
NSInteger selectedRow = [self.table selectedRow];
if (selectedRow != -1)
{
NSRect rectOfSelectedRow = [self.table rectOfRow:selectedRow];
LocationEditViewController *popupController = [[[LocationEditViewController alloc] init] autorelease];
popupController.title = #"Edit Location";
Location *locationToEdit = [self.locationController.selectedObjects objectAtIndex:0];
NSManagedObjectContext *tempContext = [[[NSManagedObjectContext alloc] init] autorelease];
tempContext.parentContext = self.document.managedObjectContext;
Location *tempLocation = (Location *)[tempContext fetchObjectEqualTo:locationToEdit]; // Custom fetch helper method
popupController.managedObjectContext = tempContext;
popupController.representedObject = tempLocation;
[popupController.popover showRelativeToRect:rectOfSelectedRow ofView:sender preferredEdge:NSMaxXEdge];
}
}
Here's the problem that I'd like an explanation for:
The text fields in the popover are connected to the popover's representedObject via bindings in the nib. These work perfectly with a new object (addLocation:).
If the Location is an existing object (tableViewDoubleClick:), the bindings work well enough to pre-populate the fields with the Location's properties. However, changing the text in the fields does not alter the Location's properties at all. When the Save button in the popup is clicked, I tried logging the Location's properties before saving the temporary context. If it's an existing object, whatever I type into the fields isn't being reflected in the Location's properties - as if the bindings are only communicating one-way.
My workaround: I found that if I skip the bindings and just manually set the Location's properties to the values in the text fields before the save, that the changes do take effect.
- (IBAction)popoverSave:(id)sender
{
// These two methods always work. But if I remove these and use bindings instead, it only works for NEW Locations.
[(Location *)self.representedObject setLabel:self.labelField.stringValue];
[(Location *)self.representedObject setLocation:self.locationField.stringValue];
NSLog(#"representedObject = %#", self.representedObject);
NSError *error = nil;
[self.managedObjectContext save:&error];
[self.popover close];
}
I'd really like to know why this is the case, just in case I'm actually doing something wrong.
Thanks!
I think it's likely that it is the cast in these lines:
[(Location *)self.representedObject setLabel:self.labelField.stringValue];
[(Location *)self.representedObject setLocation:self.locationField.stringValue];
… that makes them work. If so, then you probably have a NSObject or NSManagedObject set somewhere in the bindings as the class instead of the Location class. When the binding sends a Location class specific message e.g. set an attribute with a specific name, to the generic class, the generic class silently ignores the message.
BTW, I would caution against using multiple context instead of using the undo API. I see a lot of people get in trouble that way. It's easier to roll back a single context than it is to manage multiple context.