I have an application which renders OpenGL content on Mac OS X. Originally it was rendering to an NSOpenGLView, then I changed it to render to a CAOpenGLLayer subclass.
When I did so I saw a huge performance loss: halved framerate, lower mouse responsivity, stuttering (stops from time to time, up to a second, during which profiler activity reports waiting on mutex for data to load on GPU ram), and doubled CPU usage.
I'm investigating this issue and had a few questions:
Has a similar performance hit been seen by someone else?
Am I doing something wrong with my CAOpenGLLayer setup?
How is CAOpenGLLayer and the Core Animation framework implemented, i.e. what path does my OpenGL content do from my glDrawElements calls up to my screen, and how should I do things on my side to optimize performance with such setup?
Here's my code for CAOpenGLLayer setup:
// my application's entry point (can't be easily changed):
void AppUpdateLogic(); //update application logic. Will load textures
void AppRender(); //executes drawing
void AppEventSink(NSEvent* ev); //handle mouse and keyboard events.
//Will do pick renderings
#interface MyCAOpenGLLayer: CAOpenGLLayer
{
CGLPixelFormatObj pixelFormat;
CGLContextObj glContext;
}
#end
#implementation MyCAOpenGLLayer
- (id)init {
self = [super init];
CGLPixelFormatAttribute attributes[] =
{
kCGLPFAAccelerated,
kCGLPFAColorSize, (CGLPixelFormatAttribute)24,
kCGLPFAAlphaSize, (CGLPixelFormatAttribute)8,
kCGLPFADepthSize, (CGLPixelFormatAttribute)16,
(CGLPixelFormatAttribute)0
};
GLint numPixelFormats = 0;
CGLChoosePixelFormat(attributes, &pixelFormat, &numPixelFormats);
glContext = [super copyCGLContextForPixelFormat:mPixelFormat];
return self;
}
- (void)drawInCGLContext:(CGLContextObj)inGlContext
pixelFormat:(CGLPixelFormatObj)inPixelFormat
forLayerTime:(CFTimeInterval)timeInterval
displayTime:(const CVTimeStamp *)timeStamp
{
AppRender();
[super drawInCGLContext:inGlContext
pixelFormat:inPixelFormat
forLayerTime:timeInterval
displayTime:timeStamp ]
}
- (void)releaseCGLPixelFormat:(CGLPixelFormatObj)pixelFormat {
[self release];
}
- (CGLPixelFormatObj)copyCGLPixelFormatForDisplayMask:(uint32_t)mask {
[self retain];
return pixelFormat;
}
- (CGLContextObj)copyCGLContextForPixelFormat:(CGLPixelFormatObj)pixelFormat {
[self retain];
return glContext;
}
- (void)releaseCGLContext:(CGLContextObj)glContext {
[self release];
}
#end
#interface MyMainViewController: NSViewController {
CGLContextObj glContext;
CALayer* myOpenGLLayer;
}
-(void)timerTriggered:(NSTimer*)timer;
#end
#implementation MyMainViewController
-(void)viewDidLoad:(NSView*)view {
myOpenGLLayer = [[MyCAOpenGLLayer alloc] init];
[view setLayer:myOpenGLLayer];
[view setWantsLayer:YES];
glContext = [myOpenGLLayer copyCGLContextForPixelFormat:nil];
[NSTimer scheduledTimerWithTimeInterval:1/30.0
target:self
selector:#selector(timerTriggered:)
userInfo:nil
repeats:YES ];
}
- (void)timerTriggered:(NSTimer*)timer {
CGLContextObj oldContext = CGLContextGetCurrent();
CGLContextSetCurrent(glContext);
CGLContextLock(glContext);
AppUpdateLogic();
[myOpenGLLayer setNeedsDisplay:YES];
CGLContextUnlock(glContext);
CGLContextSetCurrent(oldContext);
}
- (void)mouseDown:(NSEvent*)event {
CGLContextObj oldContext = CGLContextGetCurrent();
CGLContextSetCurrent(glContext);
CGLContextLock(glContext);
AppEventSink(event);
CGLContextUnlock(glContext);
CGLContextSetCurrent(oldContext);
}
#end
It may be useful to know my video card isn't very powerful (Intel GMA with 64 MB of shared memory).
In one of my applications, I switched from NSOpenGLView to a CAOpenGLLayer, then ended up going back because of a few issues with the update mechanism on the latter. However, that's different from the performance issues you're reporting here.
In your case, I believe that the way you're performing the update of your layer contents may be to blame. First, using NSTimer to trigger a redraw does not guarantee that the update events will align well with the refresh rate of your display. Instead, I'd suggest setting the CAOpenGLLayer's asynchronous property to YES and using the –canDrawInCGLContext:pixelFormat:forLayerTime:displayTime: to manage the update frequency. This will cause the OpenGL layer to update in sync with the display, and it will avoid the context locking that you're doing.
The downside to this (which is also a problem with your NSTimer approach) is that the CAOpenGLLayer delegate callbacks are triggered on the main thread. If you have something that blocks the main thread, your display will freeze. Likewise, if your OpenGL frame updates take a while, they may cause your UI to be less responsive.
This is what caused me to use a CVDisplayLink to produce a triggered update of my OpenGL content on a background thread. Unfortunately, I saw some rendering artifacts when updating my CAOpenGLLayer with this, so I ended up switching back to an NSOpenGLView. Since then, I've encountered a way to potentially avoid these artifacts, but the NSOpenGLView has been fine for our needs so I haven't switched back once again.
Related
My SpriteKit game has three scenes for now; Menu.m, LevelSelect.m and Level.m. When I start the app, the memory usage is 35MB. Upon transitioning from the main menu to the level selection scene it pretty much stays around 35MB. Moving from the level select scene to the level itself it shoots up to around 150MB. Granted, there are more sprites and classes involved in creating the level. However, when I reload the level via an overlay menu that I have as a sprite the memory usage continues to rise by around 2MB for each reload. Could it be a retain cycle issue? The same is true if I switch back to the level select scene and then re-enter the level itself, the memory continues to climb and climb. I am concerned about my implementation of my state machine. I'll post some skeletal code below:
LevelScene.m
-(void)didMoveToView:(SKView *)view {
...
[self initGameStateMachine];
...
}
...
//other methods for setting up the level present
...
-(void) initGameStateMachine {
LevelSceneActiveState *levelSceneActiveState = [[LevelSceneActiveState alloc] initLevelScene:self];
LevelSceneConfigureState *levelSceneConfigureState = [[LevelSceneConfigureState alloc] initLevelScene:self];
LevelSceneFailState *levelSceneFailState = [[LevelSceneFailState alloc] initLevelScene:self];
LevelSceneSuccessState *levelSceneSuccessState = [[LevelSceneSuccessState alloc] initLevelScene:self];
NSMutableDictionary *states = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
levelSceneActiveState, #"LevelSceneActiveState",
levelSceneConfigureState, #"LevelSceneConfigureState",
levelSceneFailState, #"LevelSceneFailState",
levelSceneSuccessState, #"LevelSceneSuccessState",
nil];
_gameStateMachine = [[StateMachine alloc] initWithStates:states];
[_gameStateMachine enterState:levelSceneActiveState];
}
-(void)update:(CFTimeInterval)currentTime {
//update the on screen keyboard with notes that are being played
[_gameStateMachine updateWithDeltaTime:[NSNumber numberWithDouble:currentTime]];
}
-(void) willMoveFromView:(SKView *)view {
[self removeAllChildren];
}
StateMachine.m
#import "StateMachine.h"
#import "GameState.h"
#interface StateMachine()
#property NSMutableDictionary *states;
#end
#implementation StateMachine
-(instancetype)initWithStates:(NSMutableDictionary*)states {
if (self = [super init]) {
for (id key in [states allValues]) {
if (![key conformsToProtocol:#protocol(GameState)]) {
NSLog(#"%# does not conform to #protocol(GameState)", key);
return nil;
}
}
_states = states;
}
return self;
}
//this method will be used to start the state machine process
-(bool)enterState:(id)nextState {
if (!_currentState) {
_currentState = [_states objectForKey:[nextState className]];
[[NSNotificationCenter defaultCenter] addObserver:_currentState selector:#selector(keyPressed:) name:#"KeyPressedNotificationKey" object:nil];
return YES;
}
else if ([_currentState isValidNextState:nextState]) {
[_currentState performSelector:#selector(willLeaveState)];
_currentState = [_states objectForKey:[nextState className]];
[[NSNotificationCenter defaultCenter] addObserver:_currentState selector:#selector(keyPressed:) name:#"KeyPressedNotificationKey" object:nil];
return YES;
}
return NO;
}
-(void)updateWithDeltaTime:(NSNumber*)currentTime {
[_currentState performSelector:#selector(updateWithDeltaTime:) withObject:currentTime];
}
#end
LevelSceneActiveState //this is one of the 4 states
#import "LevelSceneActiveState.h"
#import "LevelSceneConfigureState.h"
#import "LevelSceneSuccessState.h"
#import "LevelSceneFailState.h"
#import "StateMachine.h"
#import "LevelScene.h"
#import "SSBitmapFontLabelNode.h"
#interface LevelSceneActiveState()
#property LevelScene *levelScene;
#end
#implementation LevelSceneActiveState
-(instancetype)initLevelScene:(LevelScene *)levelScene {
if (self = [super init])
_levelScene = levelScene;
return self;
}
-(void)updateWithDeltaTime:(NSNumber*)currentTime {
//game variables created here
....
//state machine needs to be set here...if set in init, it does not have a value in the LevelScene yet
if (_gameStateMachine == nil)
_gameStateMachine = _levelScene.gameStateMachine;
//game logic performed here
...
//check for timer finishing
if (!_levelScene.timer.isValid) {
//success
if (_levelScene.score >= 7) {
[_gameStateMachine enterState:LevelSceneSuccessState.self];
}
else { //failure
[_gameStateMachine enterState:LevelSceneFailState.self];
}
}
}
//another class is used to trigger notifications of key presses
-(void) keyPressed:(NSNotification*)notification {
NSNumber *keyCodeObject = notification.userInfo[#"keyCode"];
NSInteger keyCode = keyCodeObject.integerValue;
if (keyCode == 53)
[self escapePressed];
}
-(void) escapePressed {
[_gameStateMachine enterState:LevelSceneConfigureState.self];
[_levelScene childNodeWithName:#"timer"].paused = YES;
}
-(bool)isValidNextState:(id)nextState {
if ([[nextState className] isEqualToString:#"LevelSceneConfigureState"])
return YES;
...
return NO;
}
//this makes sure that we're not notifying an object that may not exist
-(void) willLeaveState {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:#"KeyPressedNotificationKey"
object:nil];
}
#end
When switching between states I do not want the LevelScene scene to go away. I understand that this is hard to diagnose especially when you're not sitting in front of the full project. What can I do to self-diagnose this myself? Any helpful tips/tricks would be great.
[UPDATE] I tried the Product->Profile->Instruments->Allocation thing, but I have no idea what to do with it. The memory is indicating that it continues to rise though.
Instrument Tutorial Article
For people that are as clueless as me when it comes to using Xcode's Instruments, here is an amazing article from Ray Wenderlich that really dumbs it down!
I found so many issues in my project that I didn't even think were issues because of that article. I'm not surprised that no one posted an answer regarding this question because when you have these types of issues they are very personal to the project that you are working on and quite hard to debug.
My Issue + Solution
My issue is a common one. I was loading a set of resources, in this case a .sf2 (sound font) file, over and over and over again when my scene reloaded. I honestly thought I had gotten rid of that issue with all of my sprites.
Here's how I found it using Instruments:
In Xcode go to Product->Profile then select Allocations
This window should pop up:
Click on the red circle button at the top left (this will start your app)
Perform the operations in your app that seem to cause issues then press Mark Generation
Repeat the operations that cause the issues and continue to press Mark Generation
Note: Mark Generation takes a snapshot of the app at that time so that you can see the changes in memory
Stop the app from running and then dive into one of the generations (I chose Generation C, because that's when the differences in memory usage became constant)
My sampler (an AVAudioUnitSampler object) shown as SamplerState is allocating a bunch of memory
I clicked on the small arrow to the right of SamplerState and it brought me into this view (shows a ton of items that are allocated memory)
Clicking on one of them plus clicking on the extended details button will allow you to see the stack trace for this item
I double clicked on the method that I thought would be the issue
After double clicking on the method, it brings up another view with your code in it along with percentages of what lines of code allocate the most memory (extremely helpful!)
Note: The culprit, in my case, was allocating roughly 1.6MB every time I reloaded the level! Ignore the commented out code, I took the screen shot of a saved session after I fixed the issue.
After fixing my code there is no longer any major memory allocations..although I still have some things to clean up! Only 33KB between level reloads, which is much better!
When I run the LayerBackedOpenGLView example on a high resolution display, the OpenGL context is not rendered at high resolution (the layer's contentScale is 1.0).
I've followed the steps in Apple's documentation, but the contentScale is still 1.0.
Specifically, I would have thought adding the following in the MyOpenGLView's init method would give me a high-resolution layer:
[self setWantsBestResolutionOpenGLSurface:YES];
But contentScale is still 1.0.
What are the steps required to update the example to render OpenGL in high resolution?
I was able to get the appropriate content scale.
I added self.wantsLayer = YES in -[MyOpenGLView init]. Even though that is set in -[MainController awakeFromNib], it doesn't seem to be early enough.
Also I added the following method to MyOpenGLView
- (void)viewDidChangeBackingProperties
{
[super viewDidChangeBackingProperties];
self.layer.contentsScale = self.window.backingScaleFactor;
}
This was necessary despite the following claim in the documentation:
When it comes to high resolution, layer-backed views are scaled automatically by the system. You don’t have any work to do to get content that looks great on high-resolution displays.
UPDATE
According to an Apple dev, this is a known bug in OS X 10.10.
Hi. This is a known bug in 10.10.0. For the moment, you can workaround it by adding this to your NSOpenGLView subclass (note this code will be harmless on older versions of Mac OS X.) Sorry for the inconvenience!
static CGFloat scaleFactorForOpenGLView(NSView *view) {
if ([view wantsBestResolutionOpenGLSurface]) {
NSWindow *window = [view window];
if (window) {
return [window backingScaleFactor];
}
}
return 1;
}
- (void)viewDidChangeBackingProperties {
[super viewDidChangeBackingProperties];
[[self layer] setContentsScale:scaleFactorForOpenGLView(self)];
}
- (CALayer *)makeBackingLayer {
CALayer *layer = [super makeBackingLayer];
[layer setContentsScale:scaleFactorForOpenGLView(self)];
return layer;
}
I have a Mac App that's been in the app store for a year or so now. It was first published with target SDK 10.7, Lion. Upon the update to Mountain Lion it no longer works.
The application displays large images in an IKImageView which is embedded in an NSScrollView. The purpose of putting it into a scrollview was to get two finger dragging working, rather than the user having to click to drag. Using ScrollViewWorkaround by Nicholas Riley, I was able to use two finger scrolling to show the clipped content after the user had zoomed in. Just like you see in the Preview app.
Nicholas Riley's Solution:
IKImageView and scroll bars
Now in Mountain Lion this doesn't work. After zooming in, pinch or zoom button, the image is locked in the lower left portion of the image. It won't scroll.
So the question is, what's the appropriate way to display a large image in IKImageView and have two finger dragging of the zoomed image?
Thank you,
Stateful
Well, Nicholas Riley's Solution is an ugly hack in that it addresses the wrong class; the issue isn't with NSClipView (which he subclassed, but which works just fine as is), but with IKImageView.
The issue with IKImageView is actually quite simple (God knows why Apple hasn't fixed this in what? … 7 years ...): Its size does not adjust to the size of the image it displays. Now, when you embed an IKImageView in an NSScrollView, the scroll view obviously can only adjust its scroll bars relative to the size of the embedded IKImageView, not to the image it contains. And since the size of the IKImageView always stays the same, the scroll bars won't work as expected.
The following code subclasses IKImageView and fixes this behavior. Alas, it won't fix the fact that IKImageView is crash-prone in Mountain Lion as soon as you zoom …
///////////////////// HEADER FILE - FixedIKImageView.h
#import <Quartz/Quartz.h>
#interface FixedIKImageView : IKImageView
#end
///////////////////// IMPLEMENTATION FILE - FixedIKImageView.m
#import "FixedIKImageView.h"
#implementation FixedIKImageView
- (void)awakeFromNib
{
[self setTranslatesAutoresizingMaskIntoConstraints:NO]; // compatibility with Auto Layout; without this, there could be Auto Layout error messages when we are resized (delete this line if your app does not use Auto Layout)
}
// FixedIKImageView must *only* be used embedded within an NSScrollView. This means that setFrame: should never be called explicitly from outside the scroll view. Instead, this method is overwritten here to provide the correct behavior within a scroll view. The new implementation ignores the frameRect parameter.
- (void)setFrame:(NSRect)frameRect
{
NSSize imageSize = [self imageSize];
CGFloat zoomFactor = [self zoomFactor];
NSSize clipViewSize = [[self superview] frame].size;
// The content of our scroll view (which is ourselves) should stay at least as large as the scroll clip view, so we make ourselves as large as the clip view in case our (zoomed) image is smaller. However, if our image is larger than the clip view, we make ourselves as large as the image, to make the scrollbars appear and scale appropriately.
CGFloat newWidth = (imageSize.width * zoomFactor < clipViewSize.width)? clipViewSize.width : imageSize.width * zoomFactor;
CGFloat newHeight = (imageSize.height * zoomFactor < clipViewSize.height)? clipViewSize.height : imageSize.height * zoomFactor;
[super setFrame:NSMakeRect(0, 0, newWidth - 2, newHeight - 2)]; // actually, the clip view is 1 pixel larger than the content view on each side, so we must take that into account
}
//// We forward size affecting messages to our superclass, but add [self setFrame:NSZeroRect] to update the scroll bars. We also add [self setAutoresizes:NO]. Since IKImageView, instead of using [self setAutoresizes:NO], seems to set the autoresizes instance variable to NO directly, the scrollers would not be activated again without invoking [self setAutoresizes:NO] ourselves when these methods are invoked.
- (void)setZoomFactor:(CGFloat)zoomFactor
{
[super setZoomFactor:zoomFactor];
[self setFrame:NSZeroRect];
[self setAutoresizes:NO];
}
- (void)zoomImageToRect:(NSRect)rect
{
[super zoomImageToRect:rect];
[self setFrame:NSZeroRect];
[self setAutoresizes:NO];
}
- (void)zoomIn:(id)sender
{
[super zoomIn:self];
[self setFrame:NSZeroRect];
[self setAutoresizes:NO];
}
- (void)zoomOut:(id)sender
{
[super zoomOut:self];
[self setFrame:NSZeroRect];
[self setAutoresizes:NO];
}
- (void)zoomImageToActualSize:(id)sender
{
[super zoomImageToActualSize:sender];
[self setFrame:NSZeroRect];
[self setAutoresizes:NO];
}
- (void)zoomImageToFit:(id)sender
{
[self setAutoresizes:YES]; // instead of invoking super's zoomImageToFit: method, which has problems of its own, we invoke setAutoresizes:YES, which does the same thing, but also makes sure the image stays zoomed to fit even if the scroll view is resized, which is the most intuitive behavior, anyway. Since there are no scroll bars in autoresize mode, we need not add [self setFrame:NSZeroRect].
}
- (void)setAutoresizes:(BOOL)autoresizes // As long as we autoresize, make sure that no scrollers flicker up occasionally during live update.
{
[self setHasHorizontalScroller:!autoresizes];
[self setHasVerticalScroller:!autoresizes];
[super setAutoresizes:autoresizes];
}
#end
I have a Mac app that's using IKImageBrowserView. I've subclassed IKImageBrowserView and I'm returning a custom cell type from newCellForRepresentedItem.
In my cell, I'm creating and returning a layer from layerForType:
// When asked for a foreground layer, return a new layer that we'll render the icon decorations into
- (CALayer *)layerForType:(NSString *)type {
if ([type isEqualToString:IKImageBrowserCellForegroundLayer]) {
#synchronized(self) {
if (!self.foregroundLayer) {
self.foregroundLayer = [[CALayer alloc] init];
self.foregroundLayer.delegate = self;
self.foregroundLayer.needsDisplayOnBoundsChange = YES;
[self.foregroundLayer setNeedsDisplay];
}
}
return self.foregroundLayer;
} else {
return [super layerForType:type];
}
}
I have my cell observing an object, and calling setNeedsDisplay on my custom layer when it changes.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.percentDone = (float)self.bytesSoFar/self.bytesTotal;
NSLog(#"Update");
[self.foregroundLayer setNeedsDisplay];
}];
}
Here's the problem I'm having: The download proceeds, the object being observed fires the observer (so observeValueForKeyPath is called) and setNeedsDisplay is called. I verified this by logging with NSLog messages.
But the drawing method:
- (void)drawLayer:(CALayer *)theLayer
inContext:(CGContextRef)theContext
{
NSLog(#"Drawing");
// Drawing happens here
}
What I'm seeing is that the drawing starts out okay - printing "Update" and "Drawing" interleaved - but after a short time, the "Drawing" stops and just the "Update" messages continue.
If I click in the image browser, or tap a key on the keyboard, the "Drawing" resumes for a short while, and then stops, back to just "Update".
It's like I need to trigger a repaint using the keyboard or mouse - the setNeedsDisplay isn't doing it - but I don't understand why. It does work for a short time, stops working, then only works while I'm providing mouse input.
This has me baffled. I'd appreciate any suggestions.
I didn't find the problem here, but I did find a solution.
Invalidating IKImageBrowserView cells doesn't cause them to be repainted because IKImageBrowserView, probably for performance, doesn't redraw cells unless their version changes.
Instead of invalidating the cell, I now increment the imageVersion returned by my IKImageBrowserItem, and then invalidate the IKImageBrowserView. This reliably redraws the item.
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
Columns in NSOutlineView in UI bound to
an NSTreeController
NSTreeControllers managedObjectContext is bound to
Application with key path of
delegate.interpretedMOC
NSTreeControllers entity is set to TrainingGroup (NSManagedObject subclass is called JGTrainingGroup)
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. It seems that no matter what I do, the NSOutlineView won't refresh.
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 to anyone who can shed any light as to why this might not be working.
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 (just the changed method)
-(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]];
// At this point if we do a fetch of all training groups, it's got the new objects included. But they don't show up in the outline view.
[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
UPDATE 1
I just found this question. I can't understand how I missed it before I posted mine, but the summary is: Core Data wasn't designed to do what I'm doing. Only one process should be using a data store.
NSManagedObjectContext and NSArrayController reset/refresh problem
However, in a different area of my application I have two processes sharing a data store with one having read only access and this seemed to work fine. Plus none of the answers to my last question on this topic mentioned that this wasn't supported in Core Data.
I'm going to re-architect my app so that only one process writes to the data store at any one time. I'm still skeptical that this will solve my problem though. It looks to me more like an NSOutlineView refreshing problem - the objects are created in the context, it's just the outline view doesn't pick them up.
I ended up re-architecting my app. I'm only importing items from one process or the other at once. And it works perfectly. Hurrah!