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