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:
In
doThing()
, register the undo action to callundoThing()
. InundoThing
, register the undo action to calldoThing()
. 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.
Wrap the calls to
doThing()
andundoThing()
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
How to Take a Snapshot of a Uiview That Isn't Rendered
How to Detect Tap on Clear Part of Uitableview
Downcast from Any to a Protocol
Nspredicate Filtered by Year Moth Day
Uialertview Is Not Working in Swift
iOS Fix Search Bar on Top of the Uitableviewcontroller
Apple Watchkit Simulator Issue: Sperrorinvalidbundlenogizmobinarymessage
iOS Calculate Text Height in Tableview Cell
How to Overlay a Skscene Over a Scnscene in Swift
Carthage Build Failed Xcode 12 12A7209 Building
Canopenurl Not Working in iOS 10
Convert Arabic String to English Number in Swift
How to Add External Webvtt Subtitles into Http Live Stream on iOS Client
Adding Views. Storyboard VS. Programmatically
Urlsessiondelegate Function Not Being Called
How to Determine the Correct Altitude for an Mkmapcamera Focusing on an Mkpolygon