NSUndoManager: capturing reference types possible?
Asked Answered
B

1

1

I am having trouble understanding how NSUndoManager captures values when the values are reference types. Whenever try to use the undoManager, the values don't undo.

Note: I need to support macOS 10.10, and I am using Swift 3 and XCode 8

Here I save the state of the ID numbers, reset them all to -1, and then try to undo. The result is that they are all still -1.

import Cocoa

class Person: NSObject {
    var id: ID 
    init(withIDNumber idNumber: Int) {
        self.id = ID(idNumber)
    }
}    

class ID: NSObject {
    var number: Int
    init(_ number: Int) {
        self.number = number
    }
}

ViewController... {

    @IBAction func setIDsToNegative1(_ sender: NSButton) {
        var savedIDs: [ID] = []
        for person in people {
            savedIDs.append(person.id)
        }

        undoManager?.registerUndo(withTarget: self, selector:#selector(undo(savedIDs:)), object: savedIDs)

        for person in people {
            person.id.number = -1
        }
    }


   func undo(savedIDs: [ID]) {
         for id in savedIDs {
              print(id.number)
         }
         //Prints -1, -1, -1
    }

}

To prove to myself that it was a problem capturing values, instead of saving the ID objects themselves, I stored the Int values that they contain. It worked perfectly as the Ints are value types.

Then I read some more about closure capturing and tried this.

undoManager?.registerUndo(withTarget: self, handler: { [saved = savedIDs] 
    (self) -> () in 
         self.undo(savedIDs: saved)
 })

Same problem, all still -1.

Brunhilde answered 10/1, 2017 at 18:19 Comment(0)
T
1

NSUndoManager isn't really about capturing about old values, but about capturing operations that restore the original values.

You are only capturing references to the IDs. So the elements in savedIDs are pointing to the same objects as the id properties on the elements of people, i.e. when you change one, you also change the other.

What you need to do is manually save the IDs and the values you want to reset them to, like so:

let savedPairs = people.map { (id: $0.id, originalValue: $0.id.number) }

This ensures that the number value itself of id is saved somewhere, not just the pointer to the id.

You can then register an action that performs the undoing with the values you captured manually, like so:

undoManager.registerUndo(withTarget: self) {
    savedPairs.forEach { $0.id.number = $0.originalValue }
}
Terret answered 12/1, 2017 at 18:48 Comment(3)
Thanks, this confirms what I suspected though, that "freeze-drying" reference states is not possible. In the example I provided the data structure is very simple so saving a tuple is easy. But what if I wanted to undo classes that had a much more complex data structures made up of other reference types? I'd have to drill down the object structure and pull out all the value types, wouldn't I? I think it's far easier to do something like what I suggested here: #24327484Brunhilde
You could do that, but I think you would be in for a bag of hurt having to manage the undo state of every single object, especially when you work on an object hierarchy. Keep in mind that you would need an undo stack for every property. Plus, this would not even help in this case as you would need to call undo() on every single ID object. Consider using value types as much as possible for your model - those will capture the value as expected.Terret
@Terret Well not necessary, what we can do is just fetch the object at previous state by conforming to NSCopying protocol and perform deep copy to restore back the state and also while adding data to the undo stack just do deep copy of the original state and add to the undo stackEklund

© 2022 - 2024 — McMap. All rights reserved.