Reputation: 529
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;
}
Upvotes: 1
Views: 713
Reputation: 15589
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;
}
Upvotes: 4