NSUndoManager was mysterious thing to me for a really long time. I wanted to
learn using it, and actually never got a time. To date. I wrote a simple app
that allows to create rectangles that can be moved around and change their
properties like background color or corner radius.
NSUndoManager allows to record operations performed by user and reverse
effect of such operations.
When you invoke some method that change something or you perform action that changes property value by e.g. set accessor you can register an operation that can reverse the action.
An undo operation consists of object that is supposed to receive a message, the message to send and the arguments - generally you want to pass original value.
The undo manager supports redo actions, so actions are reversible. You can think
of this manager as of manager that manages two stacks. Actually, it manages two
stacks, undo stack and redo stack -
_redoStack are private
NSUndoManagers where operations are stored.
When an undo operation is registered it is added to the undo stack. When
is called manager is undoing the operation, the recorded operation is performed
and it moves to redo stack, so you can redo it. When you register few undo
operations you can undo them and redo them in backward direction. You don’t
want to register operations directly onto redo stack - it is even impossible.
You can set a level of undo operations, meaning, how many undo operations a manager should store on its stack. If after adding operation the level exceeds, the oldest operations is removed from the stack.
You can check states of undo and redo stacks by
Those states are important because you might want to update UI based on stacks status.
Another case when these accessors are useful is when you have set the level of undo
operations and there is one on redo stack and limit exceeds. What
will do in this case is to remove the redo operation because it was the latest
undo operation in the history of operations.
Registering undo operations
API allows to register operation in two ways.
The first is a simple undo with
The second type of undo is undo based on
NSInvocation. You can register such
You will get a
NSUndoManagerProxy object in return and you can call on it
whatever method you want (but call only those that target conforms to,
otherwise app throws runtime exception). It will record it by creating
NSInvocation internally which will be called during undoing operation on
The important thing is that during registration the target isn’t retained. You are responsible for managing it. If such undo method is called and the target is deallocated you will get runtime exception.
In a time when object is about to be deallocated you’re responsible to call
to remove actions associated with specific target, or
remove all the operations from both undo and redo stacks.
Grouping operations is useful thing. Operations are grouped by events by default.
It means that operations are grouped around each pass of the run loop.
You can turn it off and call
Naming actions and displaying them
NSUndoManager has support for storing names of actions. You can set a name
for an action by
setActionName(_:) method. The manager comes with localization
of “Undo” and “Redo” words. There is even nice API to get ready to use strings
that conists of “Undo/Redo”.
Here is a method I created for the demo app that updates Undo and Redo buttons after every new undo action is registered or undo/redo action performed.
Manager consists of couple notification types you can observe. In the demo app
I was focused on
To let my app works perfectly I should probably observe all the will/did notifications
because operations might take a while, some part of code might be asynchronous, etc.,
and the app should display UI properly in such cases. I used those notifications
to refresh Undo/Redo buttons.
Apps can have multiple managers to be used in multiple contexts. The demo app uses two managers in two different contexts.
The first context is a board. The board is used to display rectangle on it and to move rectangles around. The possible operations on the board context are to add, move or remove a rectangle. The remove operation is accessible only as an “Undo Add Rectangle” operation.
The second context is a context of rectangle itself. You can change its color or corner radius. I decided to keep track of background color and corner radius regardless of position of a rectangle on the board.
In effect you can add a rectangle, move it, change its color and corner radius and undo moving operation without rolling back background color and corner radius. Number of contexts you should use depends on how you want your app to behave.
UIView object inherits from
UIResponder which defines interface for
objects that responds to and handles events.
UIResponder class declares
undoManager property. When the application
receives undo event,
UIResponder goes up the responder chain looking for a
responder that returns an
NSUndoManager object from undoManager. The first
that is found is used for the undo or redo operation.
To make use of a responder chain you need to override
property and return
true, and make the object that owns undo manager a first
responder by calling
becomeFirstResponder(). When you have things correctly
configured you can perform a shake gesture and the app should display alert
asking if you want to perform undo operation.
When I worked on the example I noticed that I spent more time thinking about one-purpose methods. It is very important when you want to support undo and redo actions because you want to do only specific things calling such methods.
Here is a simple code from the demo that shows how adding, removing and moving figures on the board is implemented. Here is all the code that works with undo manager.
I decided to create simple methods for registering undo operations. I noticed they work nice and logic of registering undo operation is better separated from the operation logic. Just a simple call with some parameters.
I decided also to drop
registerUndoWithTarget(_:selector:object:) method because
it based on
Selector passed with a string which is dangerous.
looks a lot better, safer and easier to use.
Although, you may need the method which takes
Selector when you want to set
You have to call directly a method you want to record. You’ll not be able to set
value of some property this way. Two ways here: The first is to expose some
method, or use
registerUndoWithTarget(_:selector:object:) method and pass
as one of parameters.
NSUndoManager is a powerful mechanism that adds undo and redo functionality
to an app in not difficult way. It requires a bit more careful way of designing
app architecture, because you have to think about one-purpose methods that you
can undo or redo as a user’s actions, but overall this is even better for apps,
right? The code is better designed.