How to Register Undomanager in Swift

How do I register UndoManager in Swift?

I tried this in a Playground and it works flawlessly:

class UndoResponder: NSObject {
@objc func myMethod() {
print("Undone")
}
}

var undoResponder = UndoResponder()
var undoManager = UndoManager()
undoManager.registerUndo(withTarget: undoResponder, selector: #selector(UndoResponder.myMethod), object: nil)
undoManager.undo()

Using NSUndoManager, how to register undos using Swift closures

What you're looking for is mutual recursion. You need two functions, each of which registers a call to the other. Here are a couple of different ways to structure it:

  1. In doThing(), register the undo action to call undoThing(). In undoThing, register the undo action to call doThing(). That is:

    @IBAction func doThing() {
    undoManager?.registerUndoWithTarget(self, handler: { me in
    me.undoThing()
    })
    undoManager?.setActionName("Thing")

    // do the thing here
    }

    @IBAction func undoThing() {
    undoManager?.registerUndoWithTarget(self, handler: { me in
    me.doThing()
    })
    undoManager?.setActionName("Thing")

    // undo the thing here
    }

Note that you should not refer to self in the closure unless you capture it with weak, because capturing it strongly (the default) may create a retain cycle. Since you're passing self to the undo manager as target, it's already keeping a weak reference for you and passing it (strongly) to the undo block, so you might as well use that and not reference self at all in the undo block.


  1. Wrap the calls to doThing() and undoThing() in separate functions that handle undo registration, and connect user actions to those new functions:

    private func doThing() {
    // do the thing here
    }

    private func undoThing() {
    // undo the thing here
    }

    @IBAction func undoablyDoThing() {
    undoManager?.registerUndoWithTarget(self, handler: { me in
    me.redoablyUndoThing()
    })
    undoManager?.setActionName("Thing")
    doThing()
    }

    @IBAction func redoablyUndoThing() {
    undoManager?.registerUndoWithTarget(self, handler: { me in
    me.undoablyDoThing()
    })
    undoManager?.setActionName("Thing")
    undoThing()
    }

UndoManager persistence in iOS

As far as I know, The UndoManager does not directly relate to your data model, hence does not directly relate to your app state - i.e the data model state.
It merely allows you to register some actions that will alter your data model in an undo/redo fashion.
To achieve persistency, while using the UndoManager, you will have to use some persistent store (a DB like Core Data or Realm, or for a very simple state you can use UserDefaults). You will have to make the actions you register with your UndoManager to update the persistent store you choose to use.
In addition, you will have to somehow keep track of the changes in the DB so you can restore and register them back between application launches.

An example I found online that describes the Undo/Redo actions registrations - taken from this example - https://medium.com/flawless-app-stories/undomanager-in-swift-5-with-simple-example-8c791e231b87

//MARK:- Add/Remove Student : Undo/Redo Actions
// Register inverse Actions that automatically performs
extension StudentsListViewController {
func addNewStudentUndoActionRegister(at index: Int){
self.undoManager?.registerUndo(withTarget: self, handler: { (selfTarget) in
selfTarget.removeNewStudent(at: index)
selfTarget.removeNewStudentUndoActionRegister(at: index)
})
}

func removeNewStudentUndoActionRegister(at index: Int){
self.undoManager?.registerUndo(withTarget: self, handler: { (selfTarget) in
selfTarget.addNewStudent(at: index)
selfTarget.addNewStudentUndoActionRegister(at: index)
})
}
}

As you can see, the actions will just change the state and the UndoManager does not keep track of the state, just the actions...

Some personal opinions:
For complex logic and state, I personally like to use some state management frameworks like Redux (or the Swift equivalent ReSwift), in this case, I can easily create an Undo/Redo actions. And you can easily keep a record of the state along the application lifecycle, and record it to a persistent store, for example, make your state Codable and keep a stack of it in storage.
It kind of bits the purpose of using the UndoManager - but as far as I know you cannot keep the registered actions to disk.

Invocation-based Undo Manager in Swift

I think the basic confusion is that prepare(withInvocationTarget:) returns a proxy object (that happens to be the undo manager itself, but that's an implementation detail). The idea is that you send this proxy object the same message(s) you send to undo the action, but instead of executing them (because it's not the actual object), it internally captures those invocations and saves them for later.

So your code should really start out something like this:

let selfProxy: Any = undoManager?.prepare(withInvocationTarget: self)

This works great in Objective-C because the "catchall" type (id) has very lax type checking. But the equivalent Any class in Swift is much more stringent and does not lend itself to the same technique, if at all.

See Using NSUndoManager and .prepare(withInvocationTarget:) in Swift 3

Implementing undo and redo in a UITextView with attributedText

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



Related Topics



Leave a reply



Submit