When you work with
NSTextView and happen to use
insertText(_:) to programmatically insert text, you get an undoable action for free.
This might give the impression you get undo/redo functionality for free.
Eventually, you’ll notice how other changes don’t have an affordance in the “Edit” menu. While it’s possible to get “Undo Typing” and “Undo Set Color” from some function calls, it’s not possible to get “Undo Change Text Attributes” when you use
That doesn’t mean that
addAttributes is broken or you’re holding it wrong – it’s just that some text view methods are exactly what is being used for user-interactive changes, and these come with auto-undo, while actual programmatic API does not.
If you look at the docs for
insertText(_:), it says:
This method is the entry point for inserting text typed by the user and is generally not suitable for other purposes. Programmatic modification of the text is best done by operating on the text storage directly. Because this method pertains to the actions of the user, the text view must be editable for the insertion to work.
So this is the same entry point for user-interactive changes, and it adheres to the rules of
isEditable. That’s not a good fit for programmatic changes.
But if you call the programmatic API of
NSTextStorage, which is just an
NSMutableAttributedString, you end up with
replaceCharacters(in:with:) and don’t get undo/redo for free anymore.
The good news is that adding undo/redo for programmatic API changes is also very simple.
UndoManager is set to group all registered blocks into 1 undoable action for each pass of the
RunLoop. That’s the main app’s while-loop that polls for events and continuously draws the app’s contents for you, basically. You are on the same “run loop pass” as long as you don’t enqueue an action asynchronously on a dispatch queue, background thread, or via
As a consequence, when you call
UndoManager.registerUndo(withTarget:handler:) in 5 different view controllers all reacting to the same notification, these 5 registered undo blocks will be coalesced into 1 undoable action because they’re in the same group on the same run loop pass.
Bottom-line: If you get undo/redo for free from calling some method on
NSTextView, chances are these were meant to be user-interactive and not programmatic API, so you should consider using a different way to achieve your goal via the underlying
NSTextStorage. That means you need to register the inverse action with the
UndoManager’s undo stack. This can be done anytime and will automatically be grouped into 1 user-undoable action, so you need to do less than you might have feared.
Update 2022-09-14: Matt Massicotte (@mattie) pointed out:
Interacting with NSTextStorage directly will not correctly take into account text selection. How much of an issue this is depends, but definitely something to be aware of.
That’s true, you have to do everything manually, including selection restoration when undoing. Maybe a good post for another day.
Receive new posts via email.