4

I'm trying to add undo and redo functionality to my implementation of a UITextView. I am using attributedText and not simply the text property of the UITextView. I've tried using the function calls in undoManager as cited in the Apple documentation, however nothing seems to be happening. I was surprised that I could not find anything on the subject by Googling. Has anyone encountered this issue before / implemented undo and redo on a UITextView with attributedText / knows how to go about this?

Sample Code

textView.attributedText = NSMutableAttributedString(string: "SOME TEXT")

@objc func undo(_ sender: UIButton) {
    textView.undoManager?.undo()
}

@objc func redo(_ sender: UIButton) {
    textView.undoManager?.redo()
}
sanch
  • 696
  • 1
  • 7
  • 21
Anters Bear
  • 1,816
  • 1
  • 15
  • 41
  • 1
    Can you include the code to show where you are registering the undo operation? – sanch May 25 '18 at 12:22
  • please make sure `IBAction` is connected to your `UIButton`.I have tested your code its working for me. – AbecedarioPoint May 25 '18 at 12:26
  • @sanch yes this is the problem, but not sure how to register all the attributes etc. @AbecedarioPoint I took your edits but actually the funcs are being called programmatically so no need for `@IBAction` – Anters Bear May 25 '18 at 12:28
  • @AntersBear means have you call `UIButton` action programatically? if yes then add `IBAction` programatically btn.addTarget(self, action: #selector(self.redo(_:)), for: .touchUpInside) – AbecedarioPoint May 25 '18 at 12:31
  • 1
    I think this will answer most of your questions. https://stackoverflow.com/a/32596899 – sanch May 25 '18 at 12:44
  • 1
    @AbecedarioPoint your edits are incorrect. Programmatic UI doesn’t need IBAction, it literally stands for InterfaceBuilderAction. Op was correct setting the objc handle since selector is an objc method and type inference is no longer implied in swift 4 – sanch May 25 '18 at 12:50
  • @sanch thanks for all your help and advice. I just implemented my own version of undo / redo from scratch using an `[Int:String]` to track content, an `[Int:NSRange]` to track selectedRange and a simple counter to keep track of the undo / redo index. Works fine alright! – Anters Bear May 25 '18 at 12:54
  • @AntersBear theis is handled pretty simply (and for "free") using an `UndoManager`. You were on the right track initially. – Ashley Mills May 25 '18 at 13:17

2 Answers2

6

Here's some sample code to handle undo/redo for a UITextView. Don't forget to update your undo/redo buttons state initially and after each change to the text.

class ViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var undoButton: UIButton!
    @IBOutlet weak var redoButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        updateUndoButtons()
    }

    @IBAction func undo(_ sender: Any) {
        textView.undoManager?.undo()
        updateUndoButtons()
    }

    @IBAction func redo(_ sender: Any) {
        textView.undoManager?.redo()
        updateUndoButtons()
    }

    func updateUndoButtons() {
        undoButton.isEnabled = textView.undoManager?.canUndo ?? false
        redoButton.isEnabled = textView.undoManager?.canRedo ?? false
    }
}        

extension ViewController: UITextViewDelegate {

    func textViewDidChange(_ textView: UITextView) {
        updateUndoButtons()            
    }
}

Obviously you'll need to hook up the actions/outlets and the text view's delegate outlet in a storyboard

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
  • Thanks! This is definitely the best solution. I already implemented my own version from scratch with my desired features / quirks, but I hope this useful to anyone who wants to implement undo / redo on `UITextView` – Anters Bear May 26 '18 at 04:55
1

This isn't the solution to OP's problem but a crude alternative

I’ve not handled this before I think you could get this by implementing a stack data structure in combination with the UITextField delegate callback textViewDidFinishEditing(textField: UITextField). The idea is that for every change the user makes to the text field, you place the current attributed string onto the stack. The undo feature comes into play by hooking up a button to your stack and popping off the most recent attributed string and setting the textfields attributed string property accordingly.

sanch
  • 696
  • 1
  • 7
  • 21
  • 1
    There's no need to write that yourself - this comes for "free". Every `UIResponder` has an optional `undoManager` which handles the pushing on / popping off the stack. – Ashley Mills May 25 '18 at 13:15
  • @AshleyMills, you are certainly correct. I've added a qualifying statement to my answer. – sanch May 25 '18 at 17:43