Using Undo in AppKit-Based Applications

The Application Kit supplements the behavior of NSUndoManager in several ways:

Undo and the Responder Chain

An application can have one or more undo clients—objects that register and perform undo operations in their local contexts. Each of these objects has its own NSUndoManager object and the associated undo and redo stacks. One example of this scenario involves custom views, each a client of an undo manager. For example, you could have a window with two custom views; each view can display text in changeable attributes (such as font, color, and size) and users can undo (or redo) each change to any attribute in either of the views. NSResponder and NSWindow define methods to help you control the context of undo operations within the view hierarchy.

NSResponder declares the undoManager method for most objects that inherit from it (namely, windows and views). When the first responder of an application receives an undo or redo message, NSResponder goes up the responder chain looking for a next responder that returns an NSUndoManager object from undoManager. Any returned undo manager is used for the undo or redo operation.

If the undoManager message wends its way up the responder chain to the window, the NSWindow object queries its delegates with windowWillReturnUndoManager: to see if the delegate has an undo manager. If the delegate does not implement this method, the window creates an NSUndoManager object for the window and all its views.

Document-based applications often make their NSDocument objects the delegates of their windows and have them respond to the windowWillReturnUndoManager: message by returning the undo manager used for the document. These applications can also make each NSWindowController object the delegate of its window—the window controller implements windowWillReturnUndoManager: to get the undo manager from its document and return it:

return [[self document] undoManager];

NSTextView

Instances of NSTextView provide undo and redo behavior. This is an optional feature, and you must make sure that when you create the text view either you select the appropriate check box in Interface Builder, or send it setAllowsUndo: with an argument of YES. If you want a text view to use its own undo manager (and not the window’s), you provide a delegate for the text view; the delegate can then return an instance of NSUndoManager from the undoManagerForTextView: delegate method.

The default undo and redo behavior applies to text fields and text in cells as long as the field or cell is the first responder (that is, the focus of keyboard actions). Once the insertion point leaves the field or cell, prior operations cannot be undone.

Undo and the Document Architecture

If you use the document architecture, some aspects of undo handling happen automatically. By default, each NSDocument object has an NSUndoManager object. (If you don’t want your application supporting Undo, you can use the NSDocument method setHasUndoManager: to prevent the creation of the undo manager.) You can use the setUndoManager: method if you need to use a subclass or if you otherwise need to change the undo manager used by the document.

When an NSDocument object has an NSUndoManager object, the document automatically keeps its edited state up to date by watching for notifications from the undo manager that tell it when changes are done, undone, or redone. In this case, you should never have to invoke the NSDocument method updateChangeCount: directly, since it is invoked automatically at the appropriate times.

The important thing to remember about supporting undo in a document-based application is that all changes that affect the persistent state of the document must be undoable. With a multilevel undo architecture, this is very important. If it is possible to make some changes to the document that cannot be undone, then the chain of edits that the NSUndoManager keeps for the document can become inconsistent with the document state. For example, imagine that you have a drawing program that is able to undo a resize, but not a delete. If the user selects a graphic and resizes it, the NSUndoManager gets an invocation that can undo that resize operation. Now the user deletes that graphic (which is not recorded for undo). If users now try to undo nothing would happen (at the very least) since the graphic that was resized is no longer there and undoing the resize can’t have any visual effect. At worst, the application might crash trying to send a message to a freed object. So when you implement undo, remember that everything that causes a change to the document should be undoable.

Undo and the Model Layer

The most important code supporting undo should be in your model layer. Each model object in your application should be able to register undo invocations for all primitive methods that change the object.

It is often useful to structure the APIs of your model object to consist of primitive methods and extended methods. Examples of this sort of separation can be found throughout the Foundation framework (including NSString, NSArray, and NSDictionary) as well as in the Sketch example project. If you have such a separation in your model objects, remember that only the primitives should register for undo since, by definition, the extended methods are implemented in terms of the primitives.

Some situations might require you to temporarily suspend undo registration for certain actions. For example, a Sketch application lets the user resize a graphic by grabbing a resize knob and dragging it. During this dragging, hundreds or thousands of changes may be made to the bounds rectangle of the selected graphic. Changing the bounds of a graphic is a primitive operation and would normally result in an undo registration. While the user is actively resizing, though, it would be better if those thousands of undo registrations did not happen. In these cases, your model object might provide API to temporarily suspend and resume some or all of its undo registration. It is up to you to decide how to handle this. Certainly, it would work if those thousands of undo registrations did happen, but it would be a tremendous waste of memory to have to remember all those intermediate rectangles when you will never have to restore one of those intermediate states.

Undo and the Control and View Layers

Although the most important part of your undo support should be in the model, there are two situations where you need some undo-related code in either your controller or view objects. The first case is when you want the Undo and Redo menu items to have more specific titles. You can use the NSUndoManager method setActionName: to give a name to the current undo group. The last invocation of setActionName: during an event cycle is the effective one. These names should reflect the intent of the user action, not the primitive operation that the action results in. Therefore, it is in your action methods that you should set action names.

It is not absolutely necessary to name an undo group. The menu items just say “Undo” and “Redo” without being specific about what is to be undone or redone. But when you do register a name it can help the user to know what will be undone or redone. It isn’t too hard to sprinkle a few calls to setActionName: in your view or controller action messages, so it is recommended that you try to give meaningful action names.

The second case where you might have some undo code in the controller or view layers is when there are some things that change that do not affect the actual state of the document but that still need to be undoable. Undoing selection changes is often such a case. For example, the Sketch application might not consider the selection to be a part of the document. In fact, if the document can have multiple views open on it, you might be able to have different selections in each one. However, you might want changes in the selection to be able to be undone for the user’s convenience and for visual continuity when the user is actually undoing things. In this case, the view that displays the graphics might keep track of the selection. It should register undo invocations as the selection changes.

Controller and view objects can come and go during the lifetime of a document object, and this is a consideration when controller-layer or view-layer events must be undoable. Your model objects typically live for the lifetime of the document and the document also owns the undo manager, so you don’t generally need to worry about what happens when the model goes away. But you may have to worry about what happens when the controller and view objects go away. If your controller or view object registers any undo invocations, you should make sure that they are cleared from the undo manager when the controller or view is deallocated. You can use the NSUndoManager method removeAllActionsWithTarget: for this purpose. Once a particular view on your document is closed, there is no point in keeping undo information about things such as selection changes for that view.