Is -makeWindowControllers the best place to initialize an NSPersistentDocument? - cocoa

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.

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];
}
}

How to access an object's NSDocument?

I can access an app-wide delegate instance using [NSApp delegate] after adding an NSObject to the mainmenu.xib, setting the name of the object to the name of my appDelegate and setting the mainmenu.xib delegate to this object.
Now, what I would like to do, is to access to an object's Document, i.e. the active NSDocument that the object "belongs" to. It would be a doc-wide delegate instance I guess. Sometimes [self document] works, but not always. Is there a generic way?
There is no need to pass a reference explicitly. You may access the document from NSViewController in the following way:
id document = self.view.window.windowController.document;
What about [[NSDocumentController sharedDocumentController] currentDocument] ?
Be careful nevertheless.
Please read
NSDocumentController currentDocument returning nil
For any sub windows that are part of the document, it turns out that it's very easy to make a very simple subclass of NSViewController and store the required information in there. These view controllers are set up within the main Document implementation so it is easy to pass the address of the NSDocument object. Any actual subview can then be controlled by a view controller that is a subclass of this "managing controller".
This solution does not work for every object, but it does take the biggest hurdle and solves my problem...

Cocoa: setters in an NSViewController view

I'm using an NSViewController class with a single view in it to display a progress indicator bar and some text fields. I'm trying to use progressIndicator setMaxValue:and theTextField setStringValue: but neither of these are doing anything.
I've done this before and I've checked and rechecked, it's fairly straightforward, the fact that it's not working makes me think that it has to do with the fact that the class is NSViewController. Which is why I tried
Timers *aTimer = [[Timers alloc] init];
[aTimer.timerNameLabel setStringValue:#"name"];
[aTimer.progressIndicator setMaxValue:x];
in the app delegate which is an NSObject class, but that didn't work either.
I've tried looking around the NSViewController documentation but I can't find anything that says it can't set those values so I don't know what's happening. What am I doing wrong?
You probably want to use -initWithNibName:bundle: instead of a regular init to initialize your custom nib.
EDIT: It seemed the problem was due to the view not being queried before getting other objects. By calling [myController view] you actually load the nib, which isn't done automatically when you initialize the view controller. So before you can use any element of the view, you need to call [myController view]

Manual binding in Cocoa

I have an ImageView which shows a lock, informing if an opened file is locked or not. I have 2 images for locked and unlocked cases. I want synchronize the displayed image with boolean value of my object representing an opened file.
To do this I want my ViewController to change the image in my ImageView depending on lock state of object. So both object and ViewController have a property "isLocked".
How can I synchronize them? It is easy in IB but I don't know how to do it programmatically. I tried in initialize method of my ViewController to use:
[ViewController bind:#"value" toObject:[ArrayController selection] withKeyPath:#"isLocked" options:nil];
But it doesn't work. In documentation it is said that I have to expose my binding before using it.
I try to put the following code in initializer method of my object:
[self exposeBinding:#"isLocked"];
But Xcode doesn't recognize this method.
Does somebody have experience with this kind of bindings establishing?
As #nick says, you want Key-Value-Observing.
[arrayController addObserver:self
forKeyPath:#"selection.isLocked"
options:NSKeyValueObservingOptionNew
context:#"this_context"]
Then when isLocked changes the -observeValueForKeyPath:ofObject:change:context: method that you have added to your viewController will be called (as long as you only manipulate isLocked in a KVC compliant way).
The options parameter lets you optionally tweak exactly what conditions will trigger the notification and what data is sent along with the notification. The context parameter is there to help you distinguish between notifications that you registered to receive and notifications your superclass registered to receive. It is optional.
Bindings seem like they might be useful to keep two values in sync. However, this is not what they do at all.
Yes, lots of things seem to give the impression that this is what they do, and there isn't much saying that this isn't what they do, also lots of people believe that this is what they do - but no, you cannot use them for this.
Only a handful of classes support bindings (they are listed here) and then, and this is the important bit, those classes only support binding their named bindings, and these bindings are not instance variables. eg NSTextField has a 'fontFamilyName' binding yet NSTextField does not have a 'fontFamilyName' property or instance variable, even a derived one. NSTextField does have a 'isBordered' property but not a binding - so you cannot bind 'isBordered'.
It does not mean anything to 'bind' an arbitrary property of an arbitrary Class.
Yes, you can bind two arbitrary values, the following code works just fine:
#import <Cocoa/Cocoa.h>
#interface SomeObject : NSObject
#property (retain,nonatomic) id someValue;
#end
#implementation SomeObject
#end
int main()
{
SomeObject *source=[SomeObject new];
SomeObject *target=[SomeObject new];
[target bind:#"someValue" toObject:source withKeyPath:#"someValue" options:0];
[source bind:#"someValue" toObject:target withKeyPath:#"someValue" options:0];
[source setSomeValue:#(42)];
NSLog(#"target: %#",[target someValue]);
[target setSomeValue:#(22)];
NSLog(#"source: %#",[source someValue]);
return 0;
}
As far as I can tell, the problem is the bit [ArrayController selection]. The first problem is that ArrayController is (or should be) a class, and getting the class's selection is probably pointless. The other problem is that even if this were an instance, you would be binding to the selection at the time of the call, which is almost certainly not what you want. You want to track the current selection as it changes.
So what you want is probably something like the following:
[myViewController bind:#"value" toObject:myArrayController withKeyPath:#"selection.isLocked" options:nil];

In NSWindowController subclass, [self document] returns null

I am using a custom subclass of NSDocument and a custom subclass of NSWindowController. The problem is that I cannot reference my custom document from my custom window controller.
In IB, in the TKDocument NIB I have File's Owner set to TKWindowController.
In my TKDocument subclass I have:
- (void) makeWindowControllers {
TKWindowController *controller = [[TKWindowController alloc] init];
[self addWindowController:controller];
}
Then in my TKWindowController subclass I overrode setDocument to make sure it was being called:
- (void) setDocument(NSDocument *) document {
NSLog(#"setDocument:%#", document);
[super setDocument:document];
}
and then (again in TKWindowController) my action which references the document itself:
- (IBAction) plotClicked:(id) sender {
TKDocument *doc = [self document];
NSLog(#"plotClicked %#", doc);
}
The NSLog in setDocument outputs the string returned by my [TKDocument description] override as I'd expect; I only put it there to see if it was being called. However, doc in plotClicked is null.
What might I have done wrong?
EDIT: I believe the problem is to do with NIBs. My Document has its own NIB with File's Owner set to the custom controller as mentioned above. The plotClicked action is fired from a menu item in MainMenu.xib. I believe it's hitting a new instance of the controller which isn't associated with the current, active document.
So, how do I link the two? My question is really this: How do I obtain a handle to the current active document (or its windowcontroller) from MainMenu.xib?
Thanks
My Document has its own NIB with File's Owner set to the custom controller as mentioned above.
The File's Owner of a document nib should be the document. Consider that suspect #1.
The plotClicked action is fired from a menu item in MainMenu.xib. I believe it's hitting a new instance of the controller which isn't associated with the current, active document.
Did you put a window controller inside your main menu nib? If not, then that isn't the problem, since you must have wired up your plotClicked: menu item to the First Responder, and the window controller and its document will be in the responder chain.
If you did, then there's the solution: delete the window controller from the MainMenu nib and hook up your menu item to the First Responder, so that the action message goes down the responder chain, which will enable it to hit the document or window controller.
How do I obtain a handle to …?
The only Handles on the Mac come from Carbon; those Handles do not exist in Cocoa.
init is not a designated initializer of NSWindowController. You want one of these: – initWithWindow:, – initWithWindowNibName:, – initWithWindowNibName:owner:, or – initWithWindowNibPath:owner:.
Also, from the docs:
In your class’s initialization method,
be sure to invoke on super either one
of the initWithWindowNibName:...
initializers or the initWithWindow:
initializer. Which one depends on
whether the window object originates
in a nib file or is programmatically
created.

Resources