This has nothing to do with Lion autosaving. It has everything to do with the fact that Apple’s template for Core Data saves the managed object context only at exit.
I’m proposing the following solution. Note that while you’re at it, you may want to move most of Core Data related code that Apple’s “shoebox” Core Data template puts in the AppDelegate. Put it in a singleton class called Database
. Expose +sharedDatabase
, and #define DB [Database sharedDatabase]
in the header. This was somewhat unrelated, but it’s worth mentioning.
Back to autosaving.
We’ll use NSNotificationCenter
and we’ll observe for the notification that our managed object context has changed.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleManagedObjectContextChange:) name:NSManagedObjectContextObjectsDidChangeNotification object:managedObjectContext];
Then we’ll save after some time, but ignore any errors. Delay is important because notification occurs while the context is dirty, and you cannot really save it at that time. Scheduling a timer is a good way to delay saving a bit to occur later when the execution comes to the runloop. Plus, it also allows us to group several changes by letting us cancel the timer in case another change flows in.
I added an NSTimer *saveDelayTimer
to instance variables of my Database
class. If you are keeping the managed object context in your app delegate, you can add it there, too.
So let’s take a look at implementations of notification handler, and timer handler.
-(void)handleManagedObjectContextChange:(NSNotification*)note { /* NSSet *updatedObjects = [[note userInfo] objectForKey:NSUpdatedObjectsKey]; NSSet *deletedObjects = [[note userInfo] objectForKey:NSDeletedObjectsKey]; NSSet *insertedObjects = [[note userInfo] objectForKey:NSInsertedObjectsKey]; */ [saveDelayTimer invalidate]; [saveDelayTimer release]; saveDelayTimer = [[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(quickSave:) userInfo:nil repeats:NO] retain]; } -(void)quickSave:(id)userInfo { // save without UI-displayed errors NSError *error = nil; [managedObjectContext save:&error]; if(!error) NSLog(@"Quicksave successful"); else NSLog(@"Quicksave failed: %@", error); [saveDelayTimer invalidate]; [saveDelayTimer release]; saveDelayTimer = nil; }
UPDATED Oct 5th 2011, 13:16
However, this is bad.
Upon saving managedObjectContext
, any currently-being-edited, but bound-via-Cocoa Bindings text fields will get unfocused. Let’s keep the focus, text selection and scroll offset!
-(void)quickSave:(id)userInfo { // store focus and selection IRAppDelegate *appDelegate = [NSApp delegate]; NSTextField * focusedTextField = nil; NSTableView * focusedTableView = nil; NSInteger focusedTableViewColumn = 0, focusedTableViewRow = 0; NSRange selection; NSRect visibleRect; if([appDelegate.mainWindowController.window.firstResponder isKindOfClass:[NSText class]]) { NSText * textBox = (NSText*)appDelegate.mainWindowController.window.firstResponder; NSTextField * textField = [textBox parentTextField]; if(textField) { // there is a text field that's focused focusedTextField = textField; selection = [textBox selectedRange]; visibleRect = [textBox visibleRect]; } NSTableView * tableView = [textBox parentTableView]; if(tableView) { // there is a table view that's focused focusedTableView = tableView; focusedTableViewColumn = [tableView editedColumn]; focusedTableViewRow = [tableView editedRow]; selection = [textBox selectedRange]; visibleRect = [textBox visibleRect]; } } [appDelegate.mainWindowController.window endEditingFor:nil]; // save without UI-displayed errors NSError *error = nil; [managedObjectContext save:&error]; if(!error) NSLog(@"Quicksave successful"); else NSLog(@"Quicksave failed: %@", error); [saveDelayTimer invalidate]; [saveDelayTimer release]; saveDelayTimer = nil; // restore selection [focusedTextField becomeFirstResponder]; [[focusedTextField currentEditor] setSelectedRange:selection]; [[focusedTextField currentEditor] scrollRectToVisible:visibleRect]; [focusedTableView becomeFirstResponder]; [focusedTableView editColumn:focusedTableViewColumn row:focusedTableViewRow withEvent:nil select:YES]; [[focusedTableView currentEditor] setSelectedRange:selection]; [[focusedTableView currentEditor] scrollRectToVisible:visibleRect]; }
This uses a small category for finding the owner of NSText
(the actual textbox that appears when you begin editing table view or a text field).
// NSText+IRFindParentTextOwner.h #import@interface NSText(IRFindParentTextOwner) -(NSTextField*)parentTextField; -(NSTableView*)parentTableView; @end
// NSText+IRFindParentTextOwner.m #import "NSText+IRFindParentTextOwner.h" @implementation NSText(IRFindParentTextOwner) -(NSTextField*)parentTextField { NSText *textBox = self; for (NSResponder *parent = textBox.nextResponder; parent; parent = parent.nextResponder) { if([parent isKindOfClass:[NSTextField class]]) { return (NSTextField*)parent; } } return nil; } -(NSTableView*)parentTableView { NSText *textBox = self; for (NSResponder *parent = textBox.nextResponder; parent; parent = parent.nextResponder) { if([parent isKindOfClass:[NSTableView class]]) { return (NSTableView*)parent; } } return nil; } @end