views:

48

answers:

2

I have a Core Data based UIKit application that allows the user the drag objects around on the screen. While an object is being dragged, I update its position attribute on each touchesMoved: event. To support undoing a drag operation in one go, I start a new undo group at the beginning of the drag, and end the group when the user lifts their finger.

To save memory and to make undo operations fast, I want to coalesce undo data belonging to the drag operation, but Core Data makes this difficult. The problem is that processPendingChanges is called at the end of each run loop cycle, and it forces Core Data to file a new undo record for the position change that happened in that iteration. A drag operation can easily accumulate hundreds of such undo records, all of which but the first are unnecessary.

Is there a way for me to keep using Core Data's magical built-in undo support, but without wasting precious memory on such duplicate undo records? I love that I don't need to care about maintaining object graph consistency across undo/redo operations, but not being able to correctly handle these continuous attribute updates seems to be a showstopper.

+3  A: 

I think setting the undo managers setGroupsByEvent: will do what you want.

Sets a Boolean value that specifies whether the receiver automatically groups undo operations during the run loop. If YES, the receiver creates undo groups around each pass through the run loop; if NO it doesn’t.

A cleaner solution might be to simply not commit the objects position to the data model until the end of the drag event.

TechZen
Sadly `setGroupsByEvent:` doesn't help here. The drag spans multiple events, so unless I misunderstood something basic, there is no way around explicitly grouping my changes with `beginUndoGrouping`/`endUndoGrouping` — and `setGroupsByEvent` becomes a no-op while there is already an open group.Not changing the model until the end of the drag gesture is probably the best way to go. The problem is that I have nowhere to store the transient position — I do my rendering directly from the model, with no separate view objects. But perhaps this just means it's time to add them now!
Fnord
IIRC, setting the `groupsByEvent` to `NO`, you get one long undo group which transcends the run loop.
TechZen
*Sigh* No, setting `groupsByEvent` to `NO` just disables implicit grouping, so you need to always start and end your own groups. Yes, groups you yourself create can obviously transcend a single run loop iteration. That wasn't the question.The question was a memory usage and performance issue arising from the automatic call to `performPendingChanges` at the end of each run loop iteration. The cleaner solution you suggested happens to be a good workaround: if the model only changes at the end of the gesture, `performPendingChanges` won't register a new undo action in each run loop iteration.
Fnord
Okay, I haven't fiddle with it in a while.
TechZen
A: 

One solution is to disable all undo registration after the very first drag event, and keep it disabled until the entire gesture is finished.

If you have groupsByEvent on, you'll need to keep in mind that the undo manager ignores all grouping messages while registration is off, including the one that ends the implicit group automatically at the end of the event. So if you plan to leave registration turned off at the end of the run loop, you'll have to manually close the implicit group yourself:

[moc processPendingChanges];
while ([moc.undoManager groupingLevel])
    [moc.undoManager endUndoGrouping];
[moc.undoManager disableUndoRegistration];

Once the drag gesture is finished, you can re-enable undo registration with the following code:

[moc processPendingChanges];
[moc.undoManager enableUndoRegistration];

This solution works, but it is a bit kludgy. The one suggested by TechZen is much cleaner: don't update model attributes until the drag gesture is done.

Fnord
It turns out the cleaner approach did not fit my application well -- systematically introducing a separate (independently tweakable) view tree in parallel to my model objects would have introduced far too many complications. I ended up using this simpler solution.
Fnord