Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using NSUndoManager, how to register undos using Swift closures

I am trying to grok how to use NSLayoutManager using Swift closures. I can successfully register an undo as follows:

doThing();
undoManager?.registerUndoWithTarget(self, handler: { _ in
    undoThing();
}
undoManager?.setActionName("do thing")

Of course I need to support redo which amounts to an undo of an undo. I can do that:

doThing();
undoManager?.registerUndoWithTarget(self, handler: { _ in
    undoThing();

    undoManager?.registerUndoWithTarget(self, handler: { _ in
        doThing();
    }
    undoManager?.setActionName("do thing")
}
undoManager?.setActionName("do thing")

But now I need to support an undo of the redo... hmmm.... ok:

doThing();
undoManager?.registerUndoWithTarget(self, handler: { _ in
    undoThing();

    undoManager?.registerUndoWithTarget(self, handler: { _ in
        doThing();

        undoManager?.registerUndoWithTarget(self, handler: { _ in
             undoThing();
        }
        undoManager?.setActionName("do thing")
    }
    undoManager?.setActionName("do thing")
}
undoManager?.setActionName("do thing")

As you can see its "turtles all the way down." How do I escape from this madness? i.e., in all the example code I can find, folks use the selector version of the code to register a method that can undo itself -- this is not obviously doable with the closure method I am using... How does one use the closure version and get unlimited undo/redo?

like image 844
wcochran Avatar asked Apr 08 '16 04:04

wcochran


1 Answers

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()
    }
    
like image 180
rob mayoff Avatar answered Nov 09 '22 12:11

rob mayoff