How can I integrate my own text changes into UITextView's undo manager?

I have an app that uses UITextView for some text editing. I have some custom operations I can do on the text that I want to be able to undo, and I'm representing those operations in a way that plugs into NSUndoManager nicely. For example, if I have a button that appends an emoji to the text, it looks something like this:

func addEmoji() {
  let inserting = NSAttributedString(string: "😀")
  self.textStorage.append(inserting)

  let len = inserting.length
  let range = NSRange(location: self.textStorage.length - len, length: len)
  self.undoManager?.registerUndo(withTarget: self, handler: { view in 
    view.textStorage.deleteCharacters(in: range)
  }
}

My goal is something like this:

  1. Type some text
  2. Press the emoji button to add the emoji
  3. Trigger undo (via gesture or keyboard shortcut) and the emoji is removed
  4. Trigger undo again and the typing from step 1 is reversed

If I just type and then trigger undo, the typing is reversed as you'd expect. And if I just add the emoji and trigger undo, the emoji is removed. But if I do the sequence above, step 3 works but step 4 doesn't. The emoji is removed but the typing isn't reversed.

Notably, if step 3 only changes attributes of the text, like applying a strikethrough to a selection, then the full undo chain works. I can type, apply strikethrough, undo strikethrough, and undo typing.

It's almost as if changing the text invalidates the undo manager's previous operations?

How do I insert my own changes into UITextView's NSUndoManager without invalidating its chain of other operations?

I've posted sample code here: https://github.com/tomhamming/TextViewUndo

By modifying a UITextView's textStorage directly, you're circumventing the middle layer that tracks updates and deletions to the text view.

Instead, I would suggest calling the various methods on UITextInput to update the underlying text storage in order to keep the undo manager in a consistent state. For example, insertText(_:) for inserting text, or replace(_:withText:) for replacing.

If you need to plumb attributes through, you may need to do this by setting typingAttributes before calling insertText(_:) or replace(_:withText:).

Hello! Thanks for the reply. Just using insertText on UIKeyInput doesn't quite work for me, because I need to insert attributed strings.

My full use case is implementing bulleted and numbered lists without using NSTextList (because I have to support OS versions before support for that), and selecting multiple lines and doing things like changing the indent level or removing list styling. The user taps one button and I have to add/remove/change list prefixes and change paragraph style attributes, and I want that to be undoable in one step. I have it set up so I can make those changes and undo and then replay it as I wish, but when I do it erases the undo manager stack.

How can I integrate my own text changes into UITextView's undo manager?
 
 
Q