Adding Undo and Redo Support in macOS Applications

Adding Undo and Redo Support in macOS Applications

When developing applications for macOS, one of the most commonly expected features by users is the ability to undo and redo actions. This isn’t just a bonus feature. For users of productivity tools, creative software, or even basic text editors, undo and redo support gives confidence that they can fix or revert mistakes. It’s a simple yet powerful part of the macOS user experience.

If you’re a developer—whether a beginner or an expert—looking to enhance user interaction in your app, supporting undo and redo is a step you shouldn’t overlook.


What This Article Covers

  • How NSUndoManager works in macOS and how to set it up in your app
  • Proper integration of undo and redo in the MVC architecture
  • Tips for UI integration, handling complex state changes, and debugging these features

Understanding macOS’s Undo Manager

NSUndoManager is a built-in class in AppKit used to manage undo and redo operations. Simply put, it’s your app’s “memory” for user actions. For example, if a user types something and wants to erase their last input, the undo manager handles recording and reversing that step.

It works by registering “inverse” actions. Every time a user does something that should be undoable, you must register how to revert it. Some built-in components like NSTextView have automatic support, but for custom features, you must manually register undo operations.

Think of it like having a secretary who takes notes of everything you do so you can say, “Let’s go back to what I did yesterday.” And yes, it can track entire chains of actions.


Getting Started: Setting Up Undo Manager

To begin, your view controller or document must have access to an NSUndoManager. If you’re using an NSDocument-based app, it automatically includes an undoManager property. Otherwise, you can manually add it to your controller:

swift

let undoManager = UndoManager()

Example function to change an object’s name:

swift

func changeName(to newName: String) {

    let oldName = self.name

    undoManager.registerUndo(withTarget: self) { target in

        target.changeName(to: oldName)

    }

    self.name = newName

}

The registerUndo(withTarget:) is key—it registers the reverse action so the app knows how to restore the previous state when undo is triggered.


Using Undo and Redo in the MVC Architecture

In macOS development, MVC (Model-View-Controller) is a commonly used pattern. It’s important to place the undo logic correctly to keep your code clear and maintainable.

Ideally, undo and redo operations belong in the Model layer. This way, even if the View or Controller changes, your business logic remains intact. For example, in a drawing app where the user adds a shape, the Model should handle the undo registration, not the View.

The Controller should simply trigger the action, while the Model manages tracking and reversing it. This helps avoid messy logic, especially in feature-rich apps.


Customizing Undo Operations

Not all undo actions are just “reversals.” Sometimes, you’ll want to provide a clearer description for the user. With setActionName(_:), you can name each undo step for a more user-friendly experience:

swift

undoManager.setActionName(“Change Name”)

So when the user clicks Undo from the menu, they’ll see “Undo Change Name” instead of a generic label.

You can also group related operations using beginUndoGrouping() and endUndoGrouping()—useful for treating multiple changes (like editing multiple fields) as a single undo step.

To reset the state, removeAllActions() clears the undo stack. Just be careful—once cleared, previous actions can’t be undone.


UI Integration for Undo and Redo

Having undo and redo logic isn’t enough—the interface needs to make these clearly accessible. In macOS, they’re typically found in the Edit menu:

  • Undo
  • Redo

You can bind these to undoManager using the responder chain. This often works automatically with NSDocument or NSTextView, but for custom components, you can implement methods like:

swift

@IBAction func undoAction(_ sender: Any?) {

    undoManager?.undo()

}

@IBAction func redoAction(_ sender: Any?) {

    undoManager?.redo()

}

Use canUndo and canRedo to enable or disable UI buttons based on whether there are actions to undo or redo.

And don’t forget keyboard shortcuts—Command+Z for undo and Shift+Command+Z for redo. macOS users expect these to work.


Handling Complex State Changes

Sometimes, changes in your app consist of multiple steps. For example, a user edits three fields, deletes one, and hits save. You may want to group all these into one undo event.

That’s where grouped actions come in. Wrap the session with:

swift

undoManager.beginUndoGrouping()

// … multiple changes

undoManager.endUndoGrouping()

This makes everything feel like a single undo step.

If you use async code or animations—say, for item deletion—make sure to register undo at the right timing to avoid confusing the user.

For dynamic data with dependencies (e.g., deleting one item reorders others), include those effects in the undo registration to maintain consistency.


Debugging and Testing Undo Functionality

Even if the undo/redo logic is simple, bugs can be tricky to find. One method is logging the undo stack during usage—print action names or stack count to confirm correct registration.

In Xcode, you can also write Unit Tests to check whether the object truly returns to its original state after undoing:

  • Change a value
  • Undo
  • Assert the value is back to the original

A common issue is retain cycles—avoid holding onto objects too strongly while registering undo. Use [weak self] when needed.


Best Practices and Tips for Undo and Redo

Undo and redo aren’t just extra features—they enhance user experience and give your macOS app a professional touch. But they aren’t always necessary. Here’s how to make the most of them:

When to Implement Undo

Not every app needs it. If your app doesn’t allow user input or changes (e.g., it’s just a data viewer), there’s no need to force the feature.

But if your app lets users edit text, move objects, or draw, then undo and redo are essential to your core workflow.

Control the Undo Stack

In heavy-use apps like design or productivity tools, prevent the undo stack from growing too large, which can hurt performance:

swift

undoManager?.levelsOfUndo = 20

This helps keep your app responsive over time.

Make It Intuitive and Predictable

Align undo behavior with user expectations. If other apps support undo in a given context (like photo editing), your app should too.

Check where user interaction happens, and ensure undo and redo work there.

Give Users Feedback

It’s not enough to have undo—you should also show users what was undone or redone. Use:

  • Updated labels (“Undid: Move Layer”)
  • Toast messages or banners
  • Highlights on restored parts

Clear visual feedback builds user trust and understanding.


How Undo and Redo Support Makes Users Happy

When undo and redo are properly implemented in your macOS app, they have a huge impact on user satisfaction. Not only do they improve usability, but they gialso ve users the confidence to experiment, edit, and work without fear of mistakes.

For forgetful, fast-clicking, or busy users, this feature can win their loyalty.

Even though the system may seem simple, a solid undo/redo setup speaks volumes about your app’s quality. If you want to be among the developers who truly value user experience, this is one detail they really notice.

Be proud of an app that cares about every click and mistake—because in the end, that’s what separates a tool they tolerate from an app they love to return to.