Where to get managed object context in NSPersistentDocument? - cocoa

It seems there's spotty information out there for using Core-data with Document based apps. I have a window controller that runs a modal window in the current document. The user enters data into a text field, which creates a mutable array of strings, which I want to use to create model objects (for a many-to-many relationship), and then save them to the core-data stack. This is the method I have in the modal window's controller.
- (IBAction)saveContext:(id)sender {
if ([tagsArray count] != 0) {
int objectcount;
for (objectcount = 0; objectcount < [tagsArray count]; objectcount ++){
Tag *singleTag = (Tag *) [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:self.managedObjectContext];
singleTag.tagname = [tagsArray objectAtIndex:objectcount];
singleTag.video = selectedFile;
NSLog(#"Tagnames %#",singleTag.tagname);
}
}
[NSApp stopModalWithCode:NSOKButton];
[self.window close];
}
Ok the compiler isn't happy with self.managedObjectContext. Understandably so, since this class doesn't have a context. The way I understand it, with a document based app you want to use only one MOC. What I don't understand is how to access the document's MOC. Apple's docs are a little unclear.
Getting a Managed Object Context
In OS X:
In an single-coordinator applications, you can get the application’s context directly from the application delegate.
In document-based applications, you can get the context directly from the document instance.
I'm embarrassed to say I don't know what this means. How do I get the context from the document instance? Is it some sort of global variable? Any help is greatly appreciated.

When you create your Modal Window pass it the documents managedObjectContext to use.
So maybe have a property in the controller class for the modal window and set that modalWindow.moc=self.managedObjectContext prior to calling modalWindow.show or whatever you are using. Assuming self is your NSPersistentDocument subclass.
You must used the documents existing MOC, don't create a new one (you can but you don't want to go there).
The documents MOC is your definitive access point for adding objects to your Core Data store.

NSPersistentDocument has a managedObjectContext method to get its managed object context:
NSManagedObjectContext *context = [yourPersistentDocument managedObjectContext];

Related

Under what conditions can an NSWindowController's document change?

My app observes the document property of its NSWindowController and performs some UI setup when it is set. Once it has been set, it would be difficult to rebuild the UI (for internal reasons) as the result of a change.
Once an NSWindowController has set its document property to an opened document, under what conditions will the system ever change that property to a new NSDocument instance (i.e., will the document ever be swapped out)? I've never observed it happening, but I can imagine features like versions or iCloud syncing causing the window controller's document to get swapped for a new document. However, the documentation on the NSWindowController lifecycle doesn't seem to touch on the issue.
It doesn't change once it has been set. NSWindowController gets its document by addWindowController method. Every new/opened document creates it's own windowController. Instance of document doesn't change with iCloud or revertChanges. It is up to you how to synchronise document with its views (redrawing).
/* Create the user interface for this document, but don't show it yet.
The default implementation of this method invokes [self windowNibName],
creates a new window controller using the resulting nib name (if it is not nil),
specifying this document as the nib file's owner, and then invokes [self addWindowController:theNewWindowController] to attach it. You can override
this method to use a custom subclass of NSWindowController or to create more
than one window controller right away. NSDocumentController invokes this method
when creating or opening new documents.
*/
// e.g. override
- (void)makeWindowControllers
{
if ([[self windowControllers] count] == 0) {
MainWindowController *controller = [[MainWindowController alloc] init];
[self addWindowController:controller];
}
}

Is -makeWindowControllers the best place to initialize an NSPersistentDocument?

When loading an existing document using NSPersistentDocument, as part of initialization I'd like to prepare some content:
NSFetchRequest *req = [NSFetchRequest fetchRequestWithEntityName:#"DocumentRoot"];
NSArray *results = [self.managedObjectContext executeFetchRequest:req error:NULL];
if (results.count) self._docRoot = [results objectAtIndex:0];
When I put this code in -init, the fetch request doesn't return any results.
I encountered this problem while refactoring the view-controller components from my NSPersistentDocument subclass to a new NSWindowController subclass. I used to handle this initialization in -windowControllerDidLoadNib:, but that isn't called anymore.
If I move the code from -init to -makeWindowControllers I get the results I expect. Is -makeWindowControllers really the right place to prepare content like this?
Based on the responses I've gotten I think I'm doing the right thing, so here's my answer to my own question.
If you're using the Core Data stack provided by NSPersistentDocument, you can not use Core Data in -init.
Instead, you should:
Put the document-initialization code directly in -windowControllerDidLoadNib: – or if you use a custom NSWindowController subclass, in -makeWindowControllers.
You may also abstract the document-initialization code into a helper method with some unique name like -setUpDocument, and call that method from -makeWindowControllers/-windowControllerDidLoadNib: instead.
If you're using a plain NSDocument, or you're setting up the Core Data stack on your own, you can set up the document model in -init.
From this question and your related question about NSArrayControllers, I'm gathering that you're doing something like this:
- (void)makeWindowControllers
{
MyWindowController* wc = [[[MyWindowController alloc] initWithWindowNibName: [self windowNibName]] autorelease];
[self addWindowController: wc];
}
When you do this, -windowControllerDidLoadNib: won't be called, because the NSDocument object isn't the Nib's owner if you init that way. If you look at NSDocument.h you'll see the following comment (see added emphasis):
/* Create the user interface for this document, but don't show it yet. The
default implementation of this method invokes [self windowNibName],
creates a new window controller using the resulting nib name (if it is
not nil), **specifying this document as the nib file's owner**, and then
invokes [self addWindowController:theNewWindowController] to attach it.
You can override this method to use a custom subclass of
NSWindowController or to create more than one window controller right
away. NSDocumentController invokes this method when creating or opening
new documents.
*/
- (void)makeWindowControllers;
If you, instead, do this:
- (void)makeWindowControllers
{
MyWindowController* wc = [[[MyWindowController alloc] initWithWindowNibName: [self windowNibName] owner: self] autorelease];
[self addWindowController: wc];
}
I believe you'll find that -windowControllerDidLoadNib: is called again. That may not help you, if you have a good reason for that Nib's owner to not be the NSDocument, but that's why -windowControllerDidLoadNib: isn't being called, and what you can do to get that behavior back. That's almost certainly a better place to be doing fetches than in init, which likely happens before all the necessary CoreData support stuff is in place. So that's one option.
If the code is not called from init that is because your document is being initialized elsewhere such as initWithContentsOfURL:ofType:error:, initForURL:withContentsOfURL:ofType:error:, initWithType:error: or initWithCoder: makeWindowControllers is not for setting up your data. Try implementing all of the above initializers and log to see which is getting called.

What is the best approach for a multi window document based Cocoa app?

My app - a document based Core Data app - is going through a second iteration and now needs multiple windows to manage several model objects. Currently it manages Events and Locations through one window and one controller. The standard generated document class acts as controller for the main window at the moment.
I now want a separate window for managing Locations model objects. It seems good design to have a separate controller (NSWindowController) for each window, but then I realised that these controllers will not have access to the Managed Object Context, which is required to access the model objects.
What is the best approach here?
EDIT:
I followed ughoavgfhw solution as follows:
Created a new XIB for Locations and added an Array Controller to load Location objects
Created a custom controller ManageLocationsController as a subclass of NSWindowController
Made the custom controller the File Owner in Locations XIB
Mapped the Array Controller's context to File Owner and keyPath document.managedObjectContext
I open the Location window with:
ManageLocationsController *aController = [[ManageLocationsController alloc] initWithWindowNibName:#"ManageLocations"];
[aController showWindow: self];
This is done from EventDocument, which is the default class generated by XCode.
When mapping the Array Controller, this left a round black exclamation mark in the keyPath field and when I open the Location window it throws an exception saying "cannot perform operation without a managed object". Obviously not good. What am I missing?
Using custom window controllers is the best way to do this. A window controller might not have direct access to the managed object context, but it has access to the document, which does. You can access it programmatically using windowController.document.managedObjectContext or from bindings with the key path document.managedObjectContext. If you want to simulate direct access to the managed object context, you could create a readonly property which loads it from the document.
// header
#property (readonly) NSManagedObjectContext *managedObjectContext;
// implementation
- (NSManagedObjectContext *)managedObjectContext {
return self.document.managedObjectContext;
}
+ (NSSet *)keyPathsForValuesAffectingManagedObjectContext {
return [NSSet setWithObject:#"document.managedObjectContext"];
}
The keyPathsForValuesAffectingManagedObjectContext method is used to tell the key-value observing system that any object observing the managedObjectContext property should be notified of changes whenever the paths it returns change.
In order for the window controllers to work properly, they must be added to the document using addWindowController:. If you are creating multiple windows when the document opens, then you should override makeWindowControllers in your document method to create the window controllers, since this will be called automatically at the right time. If you are creating window controllers upon request, you can make them in whatever method you want, just be sure to add them to the document.
[theDocument addWindowController:myNewController];
As for the little black exclamation mark in IB, you will just have to ignore that. The document property of NSWindowController is defined with the type NSDocument, but the managedObjectContext property is defined by the NSPersistentDocument subclass. IB is warning you that the property might not be there, but you know it will be so you can just ignore it.

How do I set the default selection for NSTreeController at startup?

The Background
I've built a source list (similar to iTunes et al.) in my Cocoa app.
I've got an NSOutlineView, with Value
column bound to arrangedObjects.name
key path of an NSTreeController.
The NSTreeController accesses
JGSourceListNode entities in a Core
Data store.
I have three subclasses of
JGSourceListNode - JGProjectNode,
JGGroupNode and JGFolderNode.
I have selectedIndexPaths on NSTreeController bound to an NSArray called selectedIndexPaths in my App Delegate.
On startup, I search for group nodes and if they're not found in the core data store I create them:
if ([allGroupNodes count] == 0) {
JGGroupNode *rootTrainingNode = [JGGroupNode insertInManagedObjectContext:context];
[rootTrainingNode setNodeName:#"TRAIN"];
JGProjectNode *childUntrainedNode = [JGProjectNode insertInManagedObjectContext:context];
[childUntrainedNode setParent:rootTrainingNode];
[childUntrainedNode setNodeName:#"Untrained"];
JGGroupNode *rootBrowsingNode = [JGGroupNode insertInManagedObjectContext:context];
[rootBrowsingNode setNodeName:#"BROWSE"];
JGFolderNode *childFolder = [JGFolderNode insertInManagedObjectContext:context];
[childFolder setNodeName:#"Folder"];
[childFolder setParent:rootBrowsingNode];
[context save:nil];
}
What I Want
When I start the app, I want both top level groups to be expanded and "Untrained" to be highlighted as shown:
My Window http://synapticmishap.co.uk/Window.jpeg
The Problem
I put the following code in the applicationDidFinishLaunching: method of the app delegate:
[sourceListOutlineView expandItem:[sourceListOutlineView itemAtRow:0]];
[sourceListOutlineView expandItem:[sourceListOutlineView itemAtRow:2]];
NSIndexPath *rootIndexPath = [NSIndexPath indexPathWithIndex:0];
NSIndexPath *childIndexPath = [rootIndexPath indexPathByAddingIndex:0];
[self setSelectedIndexPaths:[NSArray arrayWithObject:childIndexPath]];
but the outline view seems to not have been prepared yet, so this code does nothing.
Ideally, eventually I want to save the last selection the user had made and restore this on a restart.
The Question
I'm sure it's possible using some crazy KVO to observe when the NSTreeController or NSOutlineView gets populated then expand the items and change the selection, but that feels clumsy and too much like a work around.
How would I do this elegantly?
Elegantly? This isn't elegant but it's how I'm doing it. I just do it manually. At app quit I write this value to user defaults:
lastSelectedRow = [outlineView selectedRow]
Then at app launch I run this in app did finish launching:
[self performSelector:#selector(selectLastNoteOrCreateDefaultNote) withObject:nil afterDelay:1];
Notice I just use a delay because I noticed the same as you that "the outline view seems to not have been prepared yet". Then in that selector I use this.
[outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:lastSelectedRow] byExtendingSelection:NO];
It works but better (more elegant) solutions are welcome from me too.

Shared Objects in Cocoa

I'm working with CoreData in Cocoa (not document-based).
My problem is, that I want to access the SAME NSArrayController from different NIBs.
But every NIB has an own instance of this NSArrayController.
My question is now how I could generate sharedObjects (like the NSUserDefaultsController).
It would help me a lot. Thanks for your answers. =)
You generally don't want to share an NSArrayController between nibs. It's probably better to have multiple NSArrayController (one per NIB) which are all bound to the same underlying model. If you want this model (e.g. an NSArray) to be application global, you can expose it via the NSApplication's delegate (e.g. instantiate your custom MyAppDelegate class in MainMenu.nib and connect the NSApplication's delegate outlet to the instance of your MyAppDelegate class). In other NIBs, you can then bind an NSArrayController's contentArray binding to Shared Application.delegate.myArray (assuming MyAppDelegate exposes—via KVC-compliant methods—an NSArray binding called myArray). You are essentially using IB and the MainMenu.nib to create your singleton instance of MyAppDelegate.
Keep in mind that this approach makes unit testing your application difficult, since there are now singletons in the object graph that you can't mock or stub out during testing. It would be much better to create an NSWindowController or NSViewController for each secondary (non MainMenu.nib) NIB and bind the NSArrayControllers in those nibs to File Owner.myArray. You can then instantiate the NSWindowController or NSViewController, passing it an array (or array KVC-compliant object) before loading the secondary NIB. In this way, you can test the functionality of the nibs in isolation (using a mock or stub for the array).
I'm not really sure trying to reuse NSArrayController is the best choice (I'd need to know more about your project, but I've never ran into a situation where I'd do something like that), but you can use a static variable inside a class method like so:
+ (id)sharedObject;
{
static id object = nil;
if ( object == nil )
{
object = [[self alloc] init];
}
return object;
}
Keep in mind that this is not a true singleton, since you can still allocate additional objects of that class. You can use this guide if you really want to be strict.
Matt Gallagher has a good post on singletons and other ways to have "global" data over on his blog you may want to check out too. It's a little more clear than Apples documentation, and has a link to a header file that makes it nice and easy to create singletons out of almost any Cocoa class.
I'm actually using his header file in some of my projects, and it works great.

Resources