NSOutlineView not refreshing when objects added to managed object context from NSOperations
- by John Gallagher
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