Search code examples
iosswiftcocoacore-datansundomanager

core data: disable undo for specific attributes. Recommended approach not working


I have a textfield and a checkbox, backed by core data. Changes to the checkbox should be kept out of any undo/redo operations.

The recommend approach (found on stack overflow) is the following snippet.

@IBAction func stateDidChange(sender: NSButton?)
{
    //disable undo manager
    context.processPendingChanges()
    context.undoManager?.disableUndoRegistration()

   //set value
   let value = Bool(sender!.state == NSOnState)
   <some NSManagedObject>.flag = value

    //enable undo manager
    context.processPendingChanges()
    context.undoManager?.enableUndoRegistration()
 }

But this is not working. When the user

  1. edits the textfield,
  2. updates the checkbox,
  3. and continues editing the textfield,

then changes to the checkbox are included in the undo action.

I also tried

     NSNotificationCenter.defaultCenter().postNotificationName(NSUndoManagerCheckpointNotification, object: self.undoManager)
    self.undoManager?.disableUndoRegistration()
    //do work
    NSNotificationCenter.defaultCenter().postNotificationName(NSUndoManagerCheckpointNotification, object: self.undoManager)
    self.undoManager?.enableUndoRegistration()

I even tried it in the NSManagedObject subclass

    var flag : Bool {
    get {
        self.willAccessValueForKey("flag")
        let text = self.primitiveValueForKey("flag") as! Bool
        self.didAccessValueForKey("flag")
        return text
    }
    set {
        let context = self.managedObjectContext!
        context.processPendingChanges()
        context.undoManager?.disableUndoRegistration()

        self.willChangeValueForKey("flag")
        self.setPrimitiveValue(newValue, forKey: "flag")
        self.didChangeValueForKey("flag")

        context.processPendingChanges()
        context.undoManager?.enableUndoRegistration()

    }
}

Solution

  • Not really an answer but too long for a comment (EDIT Now amended to be a real answer). First I have seen the approach used work fine to stop some coreData actions from appearing in undo. For example I use it when creating new objects and setting initial state in code. With this approach I allow user edits of the object after that but these can never be undone back to before the initial default state of the object. So in that sense the advice you have appears correct.

    However...

    I have seen reports (but have not tested it out myself) that CoreData undo behaves differently than expected. I have heard that instead of recording reverse operations for the individual property change actions it is instead maintaining a stack of object states. If true this could match your observed behaviour.

    Consider an object with a label = A and a checkbox = NO. Set label to B with undo enabled. State is now B & NO. This can be rolled back to A & NO. Now set checkbox to YES without Undo. State is now B & YES. If undo is called now the desired state would be A & YES, but that state has never existed. The state stack on recored is

    B & YES <- current state

    B & NO - A & NO <- LIFO stack of past states

    However as I said, I have not really tested this. I did a few inconclusive tests a while back with an XML coreData store that appeared to indicate there was more to the matter than this. On the other hand I could imagine it could be true for SQLite backed stored depending on how CoreData uses the underlying SQL framework. Should be tested.

    If it is true one could speculate that it is implemented on a object by object basis and that it perhaps could be circumvented by putting the non-undoable actions in a one to one child object. That way the main object state remains consistent, in the example a label and a reference to a checkBoxObject. Then the internal state of that checkBoxObject might not matter since the reference in the main object is unchanging. But that has to be tested.

    UPDATE Further to my initial answer, I have now taken my time to test the hypothesis presented and found it to be correct. CoreData appears to implement undo as a LIFO stack of complete object states. Thus it is impossible to have selective undo of certain properties within one object.

    I have also tested the second hypothesis that these state LIFO stacks are by object and thus the matter can be circumvented by putting the non-undoable properties in a separate object linked 1 to 1 to the original object. With this setup the desired behaviour is obtained.

    The behaviour is identical both for XML and SQLite backed CoreData stores.

    I have further found that to get the desired behaviour the code modifying the coreData properties should be wrapped in an undo grouping and that -processPendingChanges should be called on the managed object context before the undo group is closed.