UndoManager Introduction

I recently needed to add undo support for a Mac app after not dealing with undo for several years. I noticed some subtle changes so I’m sharing what I learned here. I have not worked with the undo manager in an iOS app, but most of the material in this article should apply to iOS apps as well as Mac apps.

Accessing the Undo Manager

The first step to adding undo support in your app is to access the undo manager. Document-based Mac and iOS apps have an undo manager for each document. Use the undoManager property to access the document’s undo manager.

Core Data apps also have an undo manager. The managed object context has an undo manager, which you access with the undoManager property.

Registering an Undo Operation

When someone performs an action in your app that can be undone, register an undo operation for that action. Registering an undo operation adds the action to the undo stack so the action can be undone.

The easiest way to register an undo operation is to call the registerUndo method. There are two registerUndo methods: one that takes a closure (unnamed function) as an argument and one that takes a selector as an argument. The method that takes a closure is the better option for Swift developers. Swift’s closure syntax is less painful to work with than its syntax for working with Objective-C selectors.

When calling registerUndo, supply a target for the undo operation and a closure handler that calls the function to perform the undo operation.

Suppose you have a note taking app. In a note taking app people can remove notes. An important undo feature is to undo note removal. If someone accidentally removes a note, they would like to get it back.

To support undoing note removal in the app, you would write two functions: one function to remove a note and a second function to restore a removed note.

In the function to remove a note, call registerUndo. Call the function that restores the note in the handler closure.

func removeNote(note: Note) {
    undoManager.registerUndo(withTarget: self) { targetSelf in
        targetSelf.restoreNote(note)
    }
    undoManager.setActionName("Remove Note")

    // Code to remove the note goes here.
}

In the removeNote function, the code calls registerUndo. The closure is the block that starts with the brace before targetSelf. The name targetSelf is one I created. You can use whatever name you want to indicate the variable is the target. The closure calls the restoreNote function to restore the removed note. When someone removes a note and undoes it, the app restores the note by calling restoreNote.

The last line of code in the function sets an action name for the Undo menu item in the Edit menu. The code sets the menu item to Undo Remove Note.

Now let’s look at the function to restore the removed note.

func restoreNote(note: Note) {
    undoManager.registerUndo(withTarget: self) { targetSelf in
        targetSelf.removeNote(note)
    }
    undoManager.setActionName("Remove Note")

    // Code to restore the note goes here.
}

Like the removeNote function, restoreNote calls registerUndo. The main difference is the handler closure. The handler calls removeNote because removing the note is the undo operation in this case. If someone undoes removing a note and chooses Redo, the code calls removeNote to redo removing the note.

You might be wondering why the action name is Remove Note instead of Restore Note. When you undo an action in a Mac app, a Redo menu item appears in the Edit menu. In this note taking app example, when you redo, you are redoing removing a note. A menu title Redo Remove Note makes more sense than Redo Restore Note.