1
votes

I have a core data application with an NSTableView bound to an NSArrayController. I manage adding and removing objects using the array controller. I'm trying to add undo/redo support so when a person deletes an object from the table view, using a menu item, they can undo the delete.

My delete method is:

- (IBAction)removeHost:(id)sender
{
    NSInteger row = [bookmarkList selectedRow];

    // Get the object so we can get to the attributes of the host
    NSArray *a = [bookmarksController arrangedObjects];
    NSManagedObject *object = [a objectAtIndex:row];

    if (!object) return;
    NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
    NSUndoManager *undoManager = [managedObjectContext undoManager];

    if (managedObjectContext.undoManager == nil)
    {
        NSLog(@"No undo manager in app controller!");
    } else {
        NSLog(@"We've got an undo manager in app controller!");
    }

    [undoManager registerUndoWithTarget:self selector:@selector(addBookmarkObject:) object:object];
    [bookmarksController removeObject:object];
    [undoManager setActionName:@"Bookmark Delete"];
}

Deleting the object works fine, but undo does not. The Command-Z menu item is never enabled. I setup a temporary menu item and action to test the undoManager,

- (IBAction)stupidUndoRemoveHost:(id)sender
{
    NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
    NSUndoManager *undoer = [managedObjectContext undoManager];

    NSLog(@"canUndo? %hhd", [undoer canUndo]);
    NSLog(@"canRedo? %hhd", [undoer canRedo]);
    NSLog(@"isUndoRegistrationEnabled? %hhd", [undoer isUndoRegistrationEnabled]);
    NSLog(@"undoMenuItemTitle = %@", [undoer undoMenuItemTitle]);
    NSLog(@"redoMenuItemTitle = %@", [undoer redoMenuItemTitle]);

    [undoer undo];
}

Using this IBAction I can do the undo (well, sort of, it adds the object twice so clearly there's still more wrong here), but I can only do it once. If I delete another object canUndo returns 0, and stupidUndoRemoveHost does nothing.

I know I'm not understanding something here. I've read through more posts here than I can count, several blog posts, and the Apple documentation. I've done this before, but it was like ten years ago, so my skills are a bit rusty. Any help or pointers in the right direction are greatly appreciated.

Update: here is the addBookmarkObject method:

- (void)addBookmarkObject: (NSManagedObject *)object
{
    [bookmarksController addObject:object];
}

And here is windowWillReturnUndoManager from the AppDelegate:

- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window {
    // Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application.
    NSUndoManager *undoManager = [[NSUndoManager alloc] init];
    self.persistentContainer.viewContext.undoManager = undoManager;

    if (self.persistentContainer.viewContext.undoManager == nil)
    {
        NSLog(@"No undo manager!");
    } else {
        NSLog(@"We've got an undo manager!");
    }

    return self.persistentContainer.viewContext.undoManager;
}
1
Show the code for addBookmarkObject. I don't see anything wrong with the code in removeHost.Mark Szymczyk
Thanks, I've updated the question with more code.Jon Buys
The undo manager of the managed object context should handle undo, you don't have to implement it yourself. Why do you replace the undo manger in windowWillReturnUndoManager:? Is this a document based app?Willeke
It's not document based, I added that as a previous troubleshooting step. I'll remove the custom undo manager and try again.Jon Buys
@Willeke If I don't init the undoManager in the App Delegate's windowWillReturnUndoManager there's no undo manager. Logs in my App Controller's stupidUndoRemoveHost show there's no undo manager, same in the delegate.Jon Buys

1 Answers

4
votes

windowWillReturnUndoManager: is called every time Appkit wants to register an undo operation and when it wants to enable/disable the Undo menu item. If windowWillReturnUndoManager: returns a new undo manager then the undo stack is empty and the Undo menu item is disabled.

Core Data will register an undo operation when an object is removed, removeHost: shouldn't register an extra undo operation.

- (IBAction)removeHost:(id)sender
{
    [bookmarksController remove:sender];
    [undoManager setActionName:@"Bookmark Delete"];
}

The Xcode macOS Cocoa App with Core Data template has some flaws.

NSWindowDelegate method windowWillReturnUndoManager: isn't called because in the xib, the delegate of the window isn't connected to the app delegate. Fix: connect the delegate of the window to the Delegate.

self.persistentContainer.viewContext.undoManager is nil. Fix: create the undo manager once when the persistent container is created.

- (NSPersistentContainer *)persistentContainer {
    // The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it.
    @synchronized (self) {
        if (_persistentContainer == nil) {
            _persistentContainer = [[NSPersistentContainer alloc] initWithName:@"TestCDUndo"];
            [_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
                if (error != nil) {
                    …
                    abort();
                }
                self->_persistentContainer.viewContext.undoManager = [[NSUndoManager alloc] init];
            }];
        }
    }

    return _persistentContainer;
}