Undo/Redo in a Cocoa Application (Example to Undo/Redo addition & deletion of rows in a table view)

Undo/Redo functionality in a Cocoa application – NSUndoManager

This article will give you an overview of adding undo and redo functionality to a cocoa application by an example that undo/redo addition and deletion of rows in a TableView.

Lets start with How Undo Manager works?
As per Apple’s Reference Guide “You register an undo operation by specifying the object that’s changing (or the owner of that object), along with a method to invoke to revert its state, and the arguments for that method. When performing undo an NSUndoManager saves the operations reverted so that you can redo the undos.”

Basically, to undo a functionality what we need to do is reverse that functionality when undo is clicked. Similarly to undo addition of a row in table view, we need to delete the added row.
When two or more rows are added one after another, clicking undo once will delete the last added row and then clicking undo one more time will delete the row added before that. This type of functionality can be achieved by using NSUndoManager as it maintains a stack where function calls along with the arguments are stored. Hence calling undo invokes the last added method call in the undo stack of NSUndoManager.
Also, if a undo is registered while performing an undo operation, then in that case this undoing of undo operation will be registered as redo by the NSUndoManager.

Confusing?? Let’s understand this with an example.

First of all you need to set up an undo manager. Get the undo manager of your application’s window as:
NSUndoManager* undoManager = [[self window]undoManager];

You may set the level of undo (how many undo/redo possible at one time) as:
[undoManager setLevelsOfUndo:5];

Now, lets say there is a NSMutableArray “objectsArr” that is the datasource array for your table view. And there are two buttons on your Xib to add and remove a row from your Table View.

Now the IBAction method for the for Add button and Delete button should look like :

– (IBAction)clickedAdd: (NSButton*)sender
{
[self addObject:@”NewRow” atIndex:[objectsArr count]];   //addObject is a user defined instance method in our class implemented with the undo functionality
}

– (IBAction)clickedDelete: (NSButton*)sender
{
[self removeObjectAtIndex:[tableview selectedRow]];    //removeObject is another instance method that deletes the selected row
}

So, clicking Add butting will insert an object in our data source array, and clicking Delete button will remove an object from the array.

To delete and add objects in a array with undo/redo functionality, we will implement our addObject: and removeObject: methods as:

– (void)addObject:(NSString*)obj atIndex:(NSInteger)index
{
[[undoManager prepareWithInvocationTarget:self]removeObjectAtIndex:index];  //1
if(![undoManager isUndoing])   //2
{
[undoManager setActionName:@”Add Object”];   //3
}
[objectsArr insertObject:obj atIndex:index];
[tableview reloadData];
}

1). The call [[undoManager prepareWithInvocationTarget: self] removeObjectAtIndex: index]; stores a invocation to method removeObjectAtIndex: with one argument as the index of the object currently added. So that when undo is clicked this invocation will be invoked.

2). [undoManager isUndoing]. //Returns a Boolean value that indicates whether the receiver is in the process of performing its undo. The method addObject:atIndex: could itself be stored as an undo invocation of some other task (such as deletion of a object). Thus if the method is called as an undo invocation, it will return TRUE.

3). [undoManager setActionName:@”Add Object”];  //Sets the name of the action associated with the Undo or Redo command. It will change the name of Undo or Redo menu item in the Edit menu to Undo Add Object or Redo Add Object.

Now, the removeObjectAtIndex: method will be implemented as:

– (void)removeObjectAtIndex:(NSInteger)index
{
[[undoManager prepareWithInvocationTarget:self]addObject:[objectsArr objectAtIndex:index] atIndex:index];

if(![undoManager isUndoing])
{
[undoManager setActionName:@”Remove Object”];
}
[objectsArr removeObjectAtIndex:index];
[tableview reloadData];
}

The invocation to be stored as the undo operation of this method will be addObject:AtIndex: method with arguments as the value and index of the object currently being deleted.

Note. When any of the above method gets called as the undo invocation, then the registration of another undo invocation in it will result into the registration of a redo. (Undoing a undo will be a redo)

In this way you can use NSUndoManager to register undo and redo for any task. The complete AppDelegate class with NSTableView datasource/delegate methods for the sample to undo/redo addition and deletion of rows is provided below. Note, the proper bindings of IBOutlet and IBAction in the Xib is left as a task for you.

AppDelegate.h

#import <Cocoa/Cocoa.h>

@interface AppDelegate : NSObject <NSApplicationDelegate,NSTableViewDataSource,NSTableViewDelegate>
{
NSMutableArray* objectsArr;
IBOutlet NSTableView* tableview;
NSUndoManager* undoManager;
}
@property (assign) IBOutlet NSWindow *window;

– (IBAction)clickedAdd: (NSButton*)sender;
– (IBAction)clickedDelete: (NSButton*)sender;

– (void)addObject:(NSString*)obj atIndex:(NSInteger)index;
– (void)removeObjectAtIndex:(NSInteger)index;

@end

AppDelegate.m

#import “AppDelegate.h”

@implementation AppDelegate

@synthesize window = _window;

– (void)dealloc
{
[super dealloc];
}

– (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
undoManager = [[self window]undoManager];
[undoManager setLevelsOfUndo:5];

objectsArr = [[NSMutableArray alloc]initWithObjects:@”object1″,@”object2″, nil];
[tableview reloadData];
}

– (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [objectsArr count];
}

– (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
return [objectsArr objectAtIndex:row];
}

– (IBAction)clickedAdd: (NSButton*)sender
{
[self addObject:@”NewRow” atIndex:[objectsArr count]];
}

– (IBAction)clickedDelete: (NSButton*)sender
{

[self removeObjectAtIndex:[tableview selectedRow]];

}

– (void)addObject:(NSString*)obj atIndex:(NSInteger)index
{
[[undoManager prepareWithInvocationTarget:self]removeObjectAtIndex:index];
if(![undoManager isUndoing])
{
[undoManager setActionName:@”Add Object”];
}
[objectsArr insertObject:obj atIndex:index];
[tableview reloadData];
}

– (void)removeObjectAtIndex:(NSInteger)index
{
[[undoManager prepareWithInvocationTarget:self]addObject:[objectsArr objectAtIndex:index] atIndex:index];

if(![undoManager isUndoing])
{
[undoManager setActionName:@”Remove Object”];
}
[objectsArr removeObjectAtIndex:index];
[tableview reloadData];
}
@end

Written By: Neha Gupta, Software Engineer, Mindfire Solutions

Advertisements

About Neha Gupta

Software Engineer at Noida, India, Work on MAC OSX Application development with Cocoa, iOS development and Qt (Cross Platform Application Framework)

Posted on January 31, 2014, in Cocoa Application, Objective-C and tagged , , , , , , , , , . Bookmark the permalink. 3 Comments.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: