swiftmacosnsundomanager

UndoManager in NSResponder subclass (macOS)


I am trying to implement undo/redo in my model. So I made my model class a subclass of NSResponder, and then implemented the following:

note: this code is edited based on more research after comments

func setAnnotations(_ newAnnotations: [Annotation]) {
    let currentAnnotations = self.annotations

    self.undoManager.registerUndo(withTarget: self, handler: { (selfTarget) in
        selfTarget.setAnnotations(currentAnnotations)
    })

    self.annotations = newAnnotations
}

Annotation is a struct.

The code inside the closure never gets executed. Initially I noticed that undoManager is nil, but then I found this snippet:

private let _undoManager = UndoManager()
override var undoManager: UndoManager {
   return _undoManager
}

Now undoManager is no longer nil, but the code inside the closure still doesn't get executed.

What am I missing here?


Solution

  • I can't reproduce any issue now that you've made your undo register code make sense. Here is the entire code of a test app (I don't know what an Annotation is so I just used String):

    import Cocoa
    class MyResponder : NSResponder {
        private let _undoManager = UndoManager()
        override var undoManager: UndoManager {
            return _undoManager
        }
        typealias Annotation = String
        var annotations = ["hello"]
        func setAnnotations(_ newAnnotations: [Annotation]) {
            let currentAnnotations = self.annotations
            self.undoManager.registerUndo(withTarget: self, handler: { (selfTarget) in
                selfTarget.setAnnotations(currentAnnotations)
            })
            self.annotations = newAnnotations
        }
    }
    
    @NSApplicationMain
    class AppDelegate: NSObject, NSApplicationDelegate {
        let myResponder = MyResponder()
        @IBOutlet weak var window: NSWindow!
        func applicationDidFinishLaunching(_ aNotification: Notification) {
            print(self.myResponder.annotations)
            self.myResponder.setAnnotations(["howdy"])
            print(self.myResponder.annotations)
            self.myResponder.undoManager.undo()
            print(self.myResponder.annotations)
        }
    }
    

    The output is:

    ["hello"]
    ["howdy"]
    ["hello"]
    

    So Undo is working perfectly. If that's not happening for you, perhaps you are mismanaging your "model class" in some way.


    By the way, a more correct to write your registration closure is this:

        self.undoManager.registerUndo(withTarget: self, handler: {
            [currentAnnotations = self.annotations] (selfTarget) in
            selfTarget.setAnnotations(currentAnnotations)
        })
    

    This ensures that self.annotations is not captured prematurely.