Core Data takes a great weight off your shoulders as a developer by providing automatic support for undoing and redoing changes. But things get a bit gnarly the moment you step off the beaten track. So prompted by a question on Stack Overflow and my regularly forgetting the details, I proudly present a quick guide to making Core Data behave right for your model when it comes to undo management
Standard Undo Support
We should all be familiar with how this works. You set the value of a Core Data property, or insert or delete an object, and the NSManagedObjectContext automatically records this with its undo manager. When the user hits the undo menu item your objects are internally updated to their original state, and send KVO notifications to match (note that the changes do not go through any accessor methods you might have written).
Suspending Undo Registration
This is all well and good, but of course there's that vital moment where you realise some property shouldn't be available in the undo menu. There's nothing in Core Data to handle this directly; instead you need to go talk to the NSUndoManager. What's this, a handy -disableUndoRegistration method? Perfect! Except it's not that simple. Here's what you actually have to do:
[[self managedObjectContext] processPendingChanges] [[[self managedObjectContext] undoManager] disableUndoRegistration] // Make your special changes to the managed object [[self managedObjectContext] processPendingChanges] [[[self managedObjectContext] undoManager] enableUndoRegistration]
You see, to stop potentially registering 1000's of tiny changes with the undo manager all at once, Core Data intelligently batches them up into a single registration. This is performed by the -processPendingChanges method, and usually occurs as the undo manager is about to hit a checkpoint. So to side-step that mechanism, your code needs to make sure any pending changes go through before undo registration can be turned off. Then your changes can be made, and the whole process repeated again to turn registration back on. Somewhat of a hassle sure, but fairly logical in the end.
But wait, there's more! You see Core Data's automagical undo/redo support actually operates pretty similarly to a version control system. Hitting undo is a lot like telling SVN that you want to go back to the revision prior to that which you have checked out right now. Turning off undo registration doesn't remove your changes from this system, it just stops the undo manager hearing about them. The change is still undone the moment the user wants to go back to a point before you made the change.
So let's say the user makes some change to a property of a managed object. And then let's say an NSTimer comes along a little later, turns off undo registration and performs a change of its own. Now if the user hits undo, BOTH of those changes will be undone at the same time. In reality, this approach just winds up effectively tacking the change onto the end of the previous changeset.
Totally unintuitive of course, but I swear it does make sense under some circumstances!!
Stepping Outside the System
So what then if you really do need to make a change to a property that is outside the undo mechanism? For a purely transient property, not a problem! It's easy to forget that managed objects can happily use traditional instance variables. Rather than defining it in the managed object model, just create an instance variable and corresponding accessor methods. Oh, and don't forget to make use of the faulting methods to jettison the value if needed.
There's still another case remaining; what of when you need a persistent property that isn't undoable? Something whose value depends on the world outside of Core Data and so once set cannot be undone.
For example Apple's Pages application records the date a document was last printed. Printing cannot be undone (clearly!), and so this property is exempt from the undo stack. Yet it is still persisted in the document. We need something similar in Sandvox too, for noting whether a page needs publishing or not.
Sadly I don't have a complete solution yet, but continue to ponder it. Do you have an idea? Get in touch, I'd love to hear from you. I'll be updating this page as things develop. Some possibilities so far:
- Create a separate context without an undo manager. Make and save changes there as needed. Major downsides are the added complexity, including how that saving a document now requires saving to MOCs - what if the second save fails? We can't undo the first save.
- -setPrimitiveValue:forKey: will update an object's state without notifying the undo manager, but it also means the context won't notice that the object is updated, meaning it probably won't be saved.
- Assuming you're operating within the standard document architecture, any solution is going to require overriding NSDocument's change tracking in some way so that once one of these changes has taken place, -isDocumentEdited always returns YES until you save the doc. In which case the same mechanism can be used to track the updated values and ensure they are persisted.
Finally, for some applications you could just follow Wil Shipley and the Postal Service's advice. Stop trying to persuade Core Data to behave as you want. Turn it off (-[NSManagedObjectContext setUndoManager:nil]) and write your undo management code as you would for a non-Core Data app, directly in your accessor methods or controller code.