Registering Undo Operations

This article describes the two ways you register an undo operation with an undo manager.

Overview

To add an undo operation to the undo stack, you must register it with the object that performs the undo operation. NSUndoManager supports two ways to register undo operations:

In most applications a single instance of NSUndoManager belongs to an object that contains or manages other objects. This is particularly the case with desktop document-based applications, where each NSDocument object is responsible for all undo and redo operations for a document. An object such as this is often called the undo manager’s client. Each client object has its own NSUndoManager. The client claims exclusive right to alter its undoable objects so that it can record undo operations for all changes. In the specific case of documents, this scheme keeps each pair of undo and redo stacks separate so that when an undo is performed, it applies to the focal document in the application (typically the one displayed in the key window). It also relieves the individual objects in a document from having to know the identity of their undo manager or from having to track changes to themselves.

However, an object that is changed can have its own undo manager and perform its own undo and redo operations. For example, you could have a custom view that displays images dragged into it; with each successful drag operation, it registers a new undo group. If the view is then selected (that is, made first responder) and the Undo command applied, the previously displayed image would be redisplayed.

Many of the following code examples register the same method for undo and redo operations. Although that approach is convenient when undo and redo toggle a simple data value between two states, you do not have to register the same method for undo and redo operations. When what is being undone and redone is more complex—for example, the insertion and deletion of objects in an array—you could call a pair of methods in alternation, one that knows how to construct a state and another that knows how to deconstruct it. When you register a selector as an undo action, it causes the method identified by that selector to be called when the user requests that what occurred in a given context be undone. The method does not have to be the same one in which registration occurred. And the method itself could register its own undo selector. The important point is that, when registering undo and redo operations, both the data state and the selector can be varied.

Simple Undo

To record a simple undo operation, you need only invoke registerUndoWithTarget:selector:object:, giving the object to be sent the undo operation selector, the selector to invoke, and an argument to pass with that message. The target object may not be the actual object whose state is changing; instead, it may be the client object, a document or container that holds many undoable objects. The argument is an object that captures the state of the object before the change is made, as illustrated in the following example:

- (void)setMyObjectTitle:(NSString *)newTitle {
 
    NSString *currentTitle = [myObject title];
    if (newTitle != currentTitle) {
        [undoManager registerUndoWithTarget:self
                selector:@selector(setMyObjectTitle:)
                object:currentTitle];
        [undoManager setActionName:NSLocalizedString(@"Title Change", @"title undo")];
        [myObject setTitle:newTitle];
    }
}

In an undo operation, setMyObjectTitle: is invoked with the previous value. Notice that this will again invoke the registerUndoWithTarget:selector:object: method—in this case with the “new” value of myObject’s title. Since the undo manager is in the process of undoing, it is recorded as a redo operation.

Invocation-Based Undo

For other changes involving specific methods or arguments that are not objects, you can use invocation-based undo, which records an actual message to revert the target object’s state. As with simple undo, you record a message that reverts the object to its state before the change. However, in this case you do so by sending the message directly to the undo manager, after preparing it with a special message (prepareWithInvocationTarget:) to note the target, as in this example:

- (void)setMyObjectWidth:(CGFloat)newWidth height:(CGFloat)newHeight{
 
    float currentWidth = [myObject size].width;
    float currentHeight = [myObject size].height;
    if ((newWidth != currentWidth) || (newHeight != currentHeight)) {
        [[undoManager prepareWithInvocationTarget:self]
                setMyObjectWidth:currentWidth height:currentHeight];
        [undoManager setActionName:NSLocalizedString(@"Size Change", @"size undo")];
        [myObject setSize:NSMakeSize(newWidth, newHeight)];
    }
}

The prepareWithInvocationTarget: method records the argument as the target of the undo operation about to be established. Following this, you send the message that reverts the target’s state—in this case, setMyObjectWidth:height:. Because NSUndoManager does not respond to this method, forwardInvocation: is invoked, which NSUndoManager implements to record the NSInvocation object containing the target, selector, and all arguments. Performing undo thus results in self being sent a setMyObjectWidth:height: message with the original values.