4

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:

  1. [self.managedObjectContext processPendingChanges] before opening any grouping. It was still sending two notifications.
  2. 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];
}

1 Answers1

0

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.

combinatorial
  • 9,132
  • 4
  • 40
  • 58
  • It did not last long though. Undoing the non-named actions has unwanted side-effects on core data. I'm back to ground-zero. – user1712077 Oct 02 '12 at 22:52
  • Have you tried not undoing the non-named actions and only undoing the named actions? – combinatorial Oct 03 '12 at 01:03
  • I have not tried that. I've updated the post with the new code that works now. The problem was I was undoing at every notification I received that a new no-name group was open. – user1712077 Oct 03 '12 at 12:13