1337

Autosaving Core Data managed object context

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 <AppKit/AppKit.h>

@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

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

 

What is 9 + 15 ?
Please leave these two fields as-is:
IMPORTANT! To be able to proceed, you need to solve the following simple math (so we know that you are a human) :-)