NSOutlineView not refreshing when objects added to managed object context from NSOperations

Posted by John Gallagher on Stack Overflow See other posts from Stack Overflow or by John Gallagher
Published on 2010-06-06T13:49:08Z Indexed on 2010/06/06 13:52 UTC
Read the original article Hit count: 360

Background

Cocoa app using core data Two processes - daemon and a main UI Daemon constantly writing to a data store UI process reads from same data store NSOutlineView in UI is bound to an NSTreeController which is bound to Application with key path of delegate.interpretedMOC

What I want

When the UI is activated, the outline view should update with the latest data inserted by the daemon.

The Problem

Main Thread Approach

I fetch all the entities I'm interested in, then iterate over them, doing refreshObject:mergeChanges:YES. This works OK - the items get refreshed correctly. However, this is all running on the main thread, so the UI locks up for 10-20 seconds whilst it refreshes. Fine, so let's move these refreshes to NSOperations that run in the background instead.

NSOperation Multithreaded Approach

As soon as I move the refreshObject:mergeChanges: call into an NSOperation, the refresh no longer works.

When I add logging messages, it's clear that the new objects are loaded in by the NSOperation subclass and refreshed. Not only that, but they are

What I've tried

I've messed around with this for 2 days solid and tried everything I can think of.

  • Passing objectIDs to the NSOperation to refresh instead of an entity name.
  • Resetting the interpretedMOC at various points - after the data refresh and before the outline view reload.
  • I'd subclassed NSOutlineView. I discarded my subclass and set the view back to being an instance of NSOutlineView, just in case there was any funny goings on here.
  • Added a rearrangeObjects call to the NSTreeController before reloading the NSOutlineView data.
  • Made sure I had set the staleness interval to 0 on all managed object contexts I was using.

I've got a feeling this problem is somehow related to caching core data objects in memory. But I've totally exhausted all my ideas on how I get this to work.

I'd be eternally grateful of any ideas anyone else has.

Code

Main Thread Approach

// In App Delegate
-(void)applicationDidBecomeActive:(NSNotification *)notification {
    // Delay to allow time for the daemon to save
    [self performSelector:@selector(refreshTrainingEntriesAndGroups) withObject:nil afterDelay:3];
}

-(void)refreshTrainingEntriesAndGroups {
    NSSet *allTrainingGroups    = [[[NSApp delegate] interpretedMOC] fetchAllObjectsForEntityName:kTrainingGroup];
    for(JGTrainingGroup *thisTrainingGroup in allTrainingGroups)
        [interpretedMOC refreshObject:thisTrainingGroup mergeChanges:YES];

    NSError *saveError = nil;
    [interpretedMOC save:&saveError];
    [windowController performSelectorOnMainThread:@selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}

// In window controller class
-(void)refreshTrainingView {
    [trainingViewTreeController rearrangeObjects]; // Didn't really expect this to have any effect. And it didn't.
    [trainingView reloadData];
}

NSOperation Multithreaded Approach

// In App Delegate
-(void)refreshTrainingEntriesAndGroups {
    JGRefreshEntityOperation  *trainingGroupRefresh = [[JGRefreshEntityOperation alloc] initWithEntityName:kTrainingGroup];
    NSOperationQueue          *refreshQueue = [[NSOperationQueue alloc] init];
    [refreshQueue setMaxConcurrentOperationCount:1];
    [refreshQueue addOperation:trainingGroupRefresh];

    while ([[refreshQueue operations] count] > 0) {
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];

    [windowController performSelectorOnMainThread:@selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}

// JGRefreshEntityOperation.m
@implementation JGRefreshEntityOperation

@synthesize started;
@synthesize executing;
@synthesize paused;
@synthesize finished;

-(void)main {
    [self startOperation];

    NSSet *allEntities    = [imoc fetchAllObjectsForEntityName:entityName];
    for(id thisEntity in allEntities)
        [imoc refreshObject:thisEntity mergeChanges:YES];

    [self finishOperation];
}

-(void)startOperation {
    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isStarted"];
    [self setStarted:YES];
    [self setExecuting:YES];
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isStarted"];

    imoc = [[NSManagedObjectContext alloc] init];
    [imoc setStalenessInterval:0];
    [imoc setUndoManager:nil];
    [imoc setPersistentStoreCoordinator:[[NSApp delegate] interpretedPSC]];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(mergeChanges:) 
                                                 name:NSManagedObjectContextDidSaveNotification 
                                               object:imoc];
}

-(void)finishOperation {
    saveError = nil;

    [imoc save:&saveError];
    if (saveError) {
        NSLog(@"Error saving. %@", saveError);
    }

    imoc = nil;

    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];
    [self setExecuting:NO];
    [self setFinished:YES];
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

-(void)mergeChanges:(NSNotification *)notification {
    NSManagedObjectContext *mainContext = [[NSApp delegate] interpretedMOC];
    [mainContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
                                  withObject:notification
                               waitUntilDone:YES];  

}

-(id)initWithEntityName:(NSString *)entityName_ {
    [super init];
    [self setStarted:false];
    [self setExecuting:false];
    [self setPaused:false];
    [self setFinished:false];
    [NSThread setThreadPriority:0.0];
    entityName = entityName_;
    return self;
}

@end

// JGRefreshEntityOperation.h
@interface JGRefreshEntityOperation : NSOperation {
    NSString *entityName;
    NSManagedObjectContext  *imoc;
    NSError *saveError;
    BOOL started;
    BOOL executing;
    BOOL paused;
    BOOL finished;
}

@property(readwrite, getter=isStarted) BOOL started;
@property(readwrite, getter=isPaused) BOOL paused;
@property(readwrite, getter=isExecuting) BOOL executing;
@property(readwrite, getter=isFinished) BOOL finished;

-(void)startOperation;

-(void)finishOperation;

-(id)initWithEntityName:(NSString *)entityName_;

-(void)mergeChanges:(NSNotification *)notification;

@end

© Stack Overflow or respective owner

Related posts about cocoa

Related posts about core-data