Question

I have an app made of several views on which I handle Core Data entities, usually using NSTableViews connected to array controllers using bindings in the most simple and usual way. One of the view however does not use usual bindings because objects of this entity must be processed (some strings used to produce other strings) and presented in a view-based NSTableView, so they require an intermediate array to package them for my needs. On the graphic interface, I use bindings with "objectValue:key_name" (keys from the new array made of processed data).

With the other views and entities of my app, when I click on a toolbar icon, a specific view is presented and modifications to the object graph are automatically visible in the table views thanks to bindings.

When I display the view related to the specific processing, updates aren't obviously visible immediately and I must launch a (void) function that programmatically performs a new fetch, processes the result and updates the array populating the view-based table view.

Here is the problem: the table view is correctly populated on application start-up because I call the function in awakeFromNib. If I want to update the table view, I can do it by clicking on a button connected to an IBAction which calls the update function. It works as well. But when I go back to the view using the toolbar icon, it doesn't work, although calling this view is also calling the update function (meaning that in the AppDelegate class, I'm calling the function written in the specific view controller class).

At first, it crashed and the debugger revealed that the managedObjectContext was nil. That is a surprise, because it works correctly when the function is called from awakeFromNib or from the IBAction. Searching the net for explanations, I found out that I had to explicitely refer to the application's managed object context. So I've added code for this and now it's not crashing anymore, but it does nothing. The function still works fine when called from awakeFormNib or from the IBAction, but has no effect when called by the view manager through my toolbar clicking.

What have I missed? Thanks for your help. Here is the code of the specific view controller:

//////// OpplogViewController.h

#import <Cocoa/Cocoa.h>
#import "ManagingViewController.h"
@interface OpplogViewController : ManagingViewController {
IBOutlet NSTableView *opplogTableView;
IBOutlet NSArrayController *opplogController;
}
@property (strong) NSMutableArray *allOpps;
- (IBAction)refreshOpp:(id)sender;
- (void)fetchTasks:(NSString *)entityName;
@end



//////// OpplogViewController.m

#import "OpplogViewController.h"
@interface OpplogViewController ()
@end

@implementation OpplogViewController
@synthesize allOpps = _allOpps;
- (id)init {
    self = [super initWithNibName:@"OpplogViewController" bundle:nil];
    if (!self) {
        return nil;
    }
    [self setTitle:NSLocalizedString(@"Opplogview", @"")];
    _allOpps = [[NSMutableArray alloc] init];
    return self;
}

- (void)awakeFromNib {
    [self fetchTasks:@"Opplog"];
    // Opplog is the database entity to be fetched
}

- (IBAction)refreshOpp:(id)sender {
    [self fetchTasks:@"Opplog"];
}

// below is the function called to fetch data and update the table view
- (void)fetchTasks:(NSString *)entityName {
    // below: 2 lines added to avoid crash because context is nil
    id delegate = [[NSApplication sharedApplication] delegate];
    self.managedObjectContext = [delegate managedObjectContext];
    // below, we reset the array used to populate the table view
    [_allOpps removeAllObjects];
    NSFetchRequest *requestOpp = [[NSFetchRequest alloc] init];
    NSPredicate *predicate = ...some predicate... ;
    [requestOpp setEntity:[NSEntityDescription entityForName:entityName inManagedObjectContext:self.managedObjectContext]];
    [requestOpp setPredicate:predicate];
    NSError *error = nil;
    NSArray *fetchResult = [self.managedObjectContext executeFetchRequest:requestOpp error:&error];
    for (id obj in fetchResult) {
        // processing the data
        // creating a dictionary with resulting data
        NSDictionary *anOpp = ... ;
        // populating the array with the dictionaries
        [_allOpps addObject:anOpp];
    }
[_allOpps sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"date" ascending:YES]]];
[opplogController setContent:_allOpps];
[opplogTableView reloadData];
}

@end

This is the AppDelegate, which calls the update function in the Opplog view controller class if we go back to that particular view:

////// AppDelegate.h
#import <Cocoa/Cocoa.h>
@class ManagingViewController;
@interface AppDelegate : NSObject <NSApplicationDelegate> {
    NSMutableArray *viewControllers;
    IBOutlet NSToolbar *toolBar;
}
@property (unsafe_unretained) IBOutlet NSWindow *window;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
- (IBAction)changeViewController:(id)sender;
- (void)displayViewController:(ManagingViewController *)vc;


////// AppDelegate.m
#import "AppDelegate.h"
#import "OpplogViewController.h"
// also importing the controller class header of the app's other views
// ...
@implementation AppDelegate
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize managedObjectContext = _managedObjectContext;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// nothing here, was generated by Xcode
}

-(id)init {
    self = [super init];
    viewControllers = [[NSMutableArray alloc] init];
    ManagingViewController *vc;

    vc = [[OpplogViewController alloc] init];
    [vc setManagedObjectContext:[self managedObjectContext]];
    [viewControllers addObject:vc];
    // below, repeating the same as paragraph above, for each view of the app
    // ...

    return self;
}

-(void)awakeFromNib {
    [self displayViewController:viewControllers[0]];
    [toolBar setSelectedItemIdentifier:NSLocalizedString(@"ViewOpplog", @"")];
    // because the Opplog view is the view which must be displayed on startup
}

-(void)displayViewController:(ManagingViewController *)vc {
    BOOL ended = [_window makeFirstResponder:_window];
    if (!ended) {
        NSBeep();
        return;
    }
    NSView *v = [vc view];
    [_window setContentView:v];
}

-(IBAction)changeViewController:(id)sender {
    // switching views is an opportunity to save the context
    NSError *error = nil;
    [[self managedObjectContext] save:&error];
    // now managing the view switch using toolbar icons
    // the sender is the clicked toolbar item
    NSInteger i = [sender tag];
    ManagingViewController *vc = viewControllers[i];

    // We go back to the Opplog view using the first toolbar icon
    // which has a tag=0 and we call the function that gives us trouble.
    // This call is supposed to update the data on the Opplog view
    // when it will be loaded.
    if (i == 0) {
        OpplogViewController *pendingController = [[OpplogViewController alloc] init];
        [pendingController fetchTasks:@"Opplog"];
    }
    [self displayViewController:vc];
}

And this is the code of the ManagingViewController class.

////// ManagingViewController.h
#import <Cocoa/Cocoa.h>
@interface ManagingViewController : NSViewController {
    NSManagedObjectContext *__strong managedObjectContext;
}
@property (strong) NSManagedObjectContext *managedObjectContext;
@end


////// ManagingViewController.m
#import "ManagingViewController.h"
@interface ManagingViewController ()
@end

@implementation ManagingViewController
@synthesize managedObjectContext;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
return self;
}
@end
Was it helpful?

Solution

The order in which objects are sent -awakeFromNib is undefined, so it's possible your -awakeFromNib message is being sent before the Core Data stack is available. You might want to use another method such as when the view controller's -representedObject is set or message it and tell it to refresh when your app delegate's -applicationDidFinishLaunching: is called (and the stack is definitely available).

Edit based on comments

In your -changeViewController method you're alloc/init'ing a new instance of OpplogViewController then asking it to fetch. It looks like you just want to switch back to the view controller at index 0, which is the OpplogViewController instance you created initially. In this case, I'm guessing you meant to say:

if (i == 0)
  [(OppLogViewController *)vc fetchTasks:@"Opplog"];

Instead of creating a new instance and messaging it, then immediately leaking it, you want to message the one you originally created and kept around. In other words, you're telling the wrong guy to fetch.

Some additional tips: Pay attention to what I mentioned about -awakeFromNib and -init as understanding this is critical. From the NSNibAwaking Protocol docs:

Because the order in which objects are instantiated from an archive is not guaranteed, your initialization methods should not send messages to other objects in the hierarchy. Messages to other objects can be sent safely from within awakeFromNib—by which time it’s assured that all the objects are unarchived and initialized (though not necessarily awakened, of course).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top