I'm new to the NSDocument architecture and am looking to set up multiple windows (and hence multiple NSWindowController objects) for a single document.
From what I understand, NSDocument was really created to work with a single window, and it seems that the ability to have multiple windows was shoehorned in later. For example, it seems that the NSDocument should always be the file's owner for any window's NIB files. But what if I wanted to separate the window controllers from the document?
For example, in the NSDocument subclass I am currently using the code:
- (void)makeWindowControllers {
[self setMyWindowController1:[[WindowControllerType1 alloc] initWithWindowNibName:#"MyWindow" owner:self]];
[self addWindowController:[self MyWindowController1]];
}
But the NIB file "MyWindow"'s file owner is set to the NSWindowController subclass (WindowControllerType1), NOT my NSDocument subclass. In this case, whenever I look to get the document by using [[NSDocumentController sharedDocumentController] currentDocument], this ALWAYS returns nil.
I figure this can be rectified if I set the NIB file's owner to the NSDocument subclass, but then all of my outlet links break, and I'm not sure how to link to the NSWindowController subclass (WindowControllerType1), as the typical course of action (as far as I can tell) is to make the NSDocument a window controller delegate as well, which I would like to avoid!
Any suggestions?
EDIT:
Let me clarify and add some new information. I am aware of Apple's position on using the WindowController's document property. However, as I plan of having a larger number of nested NSViews in each window, I want to avoid passing the document through a large chain of views in order to accomplish this.
The issue is not necessarily this chain. It is mostly that when the [[NSDocumentController sharedDocumentController] currentDocument] is ALWAYS nil, none of the "for free" features of NSDocument seem to work, such as undo/redo. This is the major issue that I need to resolve.
From what I understand, NSDocument was really created to work with a single window, and it seems that the ability to have multiple windows was shoehorned in later.
No, makeWindowControllers is available in OS X v10.0 and later.
But what if I wanted to separate the window controllers from the document?
The window controller owns the NIB.
Any suggestions?
Do
[self setMyWindowController1:[[WindowControllerType1 alloc] initWithWindowNibName:#"MyWindow"]].
NSWindowController has a property document which is set by addWindowController:.
Use document property of NSWindowController instead of currentDocument.
Related
I'm creating my first NSDocument based application. I'm able to create new documents, both from scratch and by importing legacy files.
This app will allow multiple windows per document, so I am overriding makeWindowControllers. This method is currently very simple:
- (void) makeWindowControllers
{
if (documentDatabase == nil) return;
DataSheetWindowController * dswc = [[DataSheetWindowController alloc] initWithDatabase:documentDatabase];
[self addWindowController: dswc];
}
The window appears as expected, however, the Save, Revert to Save, and other document enabled menus are disabled, as if the document was not in the responder chain.
As an experiment, I tried adding this method to my NSWindowController class:
- (void)saveDocument:(id)sender {
[[self document] saveDocument:sender];
}
With this method in place, the Save menu item is enabled, and selecting it causes the document's save methods to be invoked.
From reading the documentation and other questions on Stack Overflow, it's clear that something is wrong -- I should NOT have to put this method in the NSWindowController class. I'm sure I've overlooked something silly, but for the life of me I cannot figure out what it is, or any other mention of this problem here or elsewhere on the web.
Some additional information that may be useful -- in IB, the window's owner and delegate are set to the NSWindowController. I created a method to display the responder chain (see How to inspect the responder chain?) and the document was not listed. Here is the output of the responder chain (however, since NSDocument is not a subclass of NSResponder, I'm not sure if it is supposed to be listed anyway).
RESPONDER CHAIN:
<NSClipView: 0x102344350>
<NSScrollView: 0x102344480>
<NSView: 0x102345040>
<NSWindow: 0x10234e090>
Since the saveDocument method I put into the NSWindowController class does work, that indicates to me that the window controller does know that it is associated with the document.
So -- any thoughts as to why the document is behaving as if it is not in the responder chain?
Updated info: After setting up a new document, the initWithType method includes this temporary line to make sure that the document status is edited:
[self updateChangeCount:NSChangeDone];
I have verified that isDocumentEdited returns true.
I'm going to suggest that the solution is the one pointed to here:
https://stackoverflow.com/a/9349636/341994
In the nib containing the window that the window controller will be loading, the File's Owner proxy needs to be of the window controller's class (select the File's Owner proxy and examine the Identity inspector to confirm / configure that), and its window outlet must be hooked to the window and the window's delegate outlet must be hooked to the File's Owner proxy (select the File's Owner proxy and examine the Connections inspector to confirm that).
I would like to track each time a certain window appears (becomes visible to the user) in a OS X app. Where would be the most adequate place to call the tracker?
windowWillLoad, maybe?
I expected to find something like windowWillAppear but it seems I'm thinking too much iOS.
How about getting notification such as NSWindowDidBecomeMainNotification, By main I guess the one which is top most on screen directly visible by user.
see : Apple Documentation
Yes, one would expect that a window would notify its delegate or its controller with a windowWillAppear or windowDidAppear message, or post a documented notification like NSWindowDidAppearNotification. But alas, none of those exist. I filed a bug report with Apple and was given the advice to use a storyboard and a view controller instead. This is unhelpful in legacy apps that already use a bunch of window controllers and xibs.
You could subclass NSWindow and override orderWindow:relativeTo: to send a notification. Most, but not quite all, of the messages that make a window show itself ultimately go through this method, including orderBack:, orderFront:, makeKeyAndOrderFront:, and -[NSWindowController showWindow:]. But orderFrontRegardless does not go through orderWindow:relativeTo:, so you would also want to override that for completeness.
Another way to be notified is to make a subclass of NSViewController that controls some view that's always visible in the window. The view controller will receive viewWillAppear and viewDidAppear.
If you're subclassing NSWindow or NSViewController already for some other reason, either of these is a reasonable solution.
If you're not subclassing NSWindow already, and don't have an NSViewController subclass for a view that's always visible in the window, then another way is to use Cocoa bindings to connect the window's visible binding to a property one of your objects. For example, I have a custom NSWindowController subclass. I gave it a windowIsVisible property:
#interface MyWindowController ()
#property (nonatomic) BOOL windowIsVisible;
#end
and I implemented the accessors like this:
- (BOOL)windowIsVisible { return self.window.visible; }
- (void)setWindowIsVisible:(BOOL)windowIsVisible {
NSLog(#"window %# became %s", self.window, windowIsVisible ? "visible" : "hidden");
}
and in awakeFromNib, I bind the window's visible binding to the property like this:
- (void)awakeFromNib {
[super awakeFromNib];
[self.window bind:NSVisibleBinding toObject:self withKeyPath:NSStringFromSelector(#selector(windowIsVisible)) options:nil];
}
When the window becomes visible, the setWindowIsVisible: setter is called with an argument of YES. Note that if the whole app is hidden and reappears, the setter is called again, even though it wasn't called with argument NO when the app was hidden. So be careful not to assume the window was previously hidden.
Also, the binding might create a retain cycle, so you should probably unbind it when the window is closed, unless you want to keep the window and controller around. Note that the window does post NSWindowWillCloseNotification when it's closing, so you don't need any special magic to detect that.
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...
How can I load a nib inside of another window?
I tried initWithWindowName,
if (mmController == NULL)
mmController = [[mainMenu alloc] initWithWindowNibName:#"mainMenu"];
[mmController showWindow:self];
but it opens a new window.
I also tried loadNibNamed
[NSBundle loadNibNamed:#"mainGame" owner:self];
and it succeeded, but when I try to use the same method to get back to the main menu,
[NSBundle loadNibNamed:#"mainMenu" owner:self];
it doesn't work. It does nothing at all...
Any ideas?
I tried initWithWindowName,
You mean initWithWindow¹Nib²Name³:, which takes the name (3) of a nib (2) containing a window (1).
if (mmController == NULL)
This should be nil, not NULL, since you are comparing an Objective-C object pointer.
mmController = [[mainMenu alloc] initWithWindowNibName:#"mainMenu"];
What is mainMenu here? It must be a class, but what is it a subclass of?
[mmController showWindow:self];
From this message and the previous message, I'm guessing mainMenu is a subclass of NSWindowController.
Guessing should not be required. You should name your classes specifically, so that anybody can tell what the class is and its instances are merely by the class name.
Brevity is a virtue, but if you need to go long, go long. We've got modern tools with name completion. The tab key can eliminate the sole advantage of an abbreviated name.
but it opens a new window.
Yes. You created a window by loading it from a nib, and then you told the window controller to show that window. Showing a new window is the expected result.
I also tried loadNibNamed
[NSBundle loadNibNamed:#"mainGame" owner:self];
and it succeeded, but when I try to use the same method to get back to the main menu,
There is no “get back”. Loading a nib is simply creating objects by loading them from an archive. You can load the same nib multiple times, and loading a nib does not somehow undo the results of loading a previous nib.
You may want to read the Resource Programming Guide, which covers nibs as well as image and sound files, and the Bundle Programming Guide.
If you want to hide the window you loaded from the mainGame nib, do that. The term for this in AppKit is “ordering out” (as opposed to “ordering in”, which “ordering front” and “ordering back” are specific ways of doing).
[NSBundle loadNibNamed:#"mainMenu" owner:self];
it doesn't work. It does nothing at all...
Are you trying to load the MainMenu nib that came with your project? If so, make sure you get the case right—you don't want your app to be broken for people who run it from a case-sensitive volume, nor do you want it to be broken for people who use the default case-insensitive file-system.
If that's not what you're trying to do, then it isn't clear what you are trying to do. MainMenu is normally the nib containing the main menu (the contents of the menu bar); naming any other nib “mainMenu” or anything like that is going to cause confusion at best and problems at worst. If this is supposed to be some other nib, you should give it a different name.
Either way, this is not what you need to do. If you want to hide the window you loaded from mainGame, then you need to hide that window, not load a different nib.
Moreover, once the window is loaded, do not load it again (unless you close and release it). Once you have loaded it, you can simply order it back in. Most probably, you will want to both make it key and order it front.
On the Mac, you are not limited to one window at a time; indeed, your app has multiple windows (at least three), no matter what you do. The APIs are built around your ability to show multiple windows.
See the Window Programming Guide for more information.
How can I load a nib inside of another window?
As Justin Meiners already told you, you may want NSViewController for that, although you can go without and just load the nib containing the view directly using loadNibNamed:.
Be warned that NSViewController is not nearly as powerful/featureful as Cocoa Touch's UIViewController.
You'll want to read the View Programming Guide for this.
I have used NSWindowController in projects several times, and feel like I have a (very)rough grasp of the concepts behind this important class. What I would like to do with this post is to clarify/correct my own understandings, and hopefully help other learners get that first step into understanding. It's the at-a-glance concepts, overview, and best practices that I find is most useful, and often lacking in the documentation. Here is my take on NSWindowController (questions are interspersed in bold):
An NSWindowController (NSWC) subclass exists (conceptually) just beneath every window nib, acting as the glue between the user interface elements and the model objects that they control/represent. Basically, every window in your application should have its own NSWC subclass.
The File's Owner of the nib should always be the NSWC subclass. Is this the case even for the MainMenu.xib application?
The NSWC window property should always be linked to the NSWindow in InterfaceBuilder.
You should override the 'init' method, using [super initWithWindowNibName:], so that when you refer to [mycontroller window] it will load the nib. Should this also be the case for the NSWC for the MainMenu.xib window, even though this is opened at startup?
The NSWC shouldn't do too much heavy lifting - it should simply pass messages to instances of objects, and present those objects in the UI.
It can modify the UI using binding, or acting as a delegate for tables etc., or by actively changing the UI elements when it observes a change, or a combo of any of the above (which one you use seems to be a matter of taste, with pros and cons on all sides).
An NSWC can create instances of other NSWCs when necessary (for example, when opening a one-off sub-window).
Use the [mycontroller showWindow:nil] to display the associated window at the front. If you want the window to appear as a sheet, use something like:
NSWindowController* mycontroller = [[MyController alloc] init];
[NSApp beginSheet: [mycontroller window]
modalForWindow: [self window]
modalDelegate: self
didEndSelector: #selector(didEndMySheet:returnCode:contextInfo:)
contextInfo: nil];
The didEndSelector: should be a method of the NSWC of the parent window, and can access and release 'mycontroller' with [sheet windowController].
- To close the window call the performClose: method of NSWC's window.
Some Questions:
Should the NSWC of the MainMenu window also be the application delegate, or should this be a different class?
In the same vein, should the main NSWC handle files (drag/drop and opening), or should it be passed on to the app delegate, or is that just a matter of taste?
Please correct me if any of this is bad practice, or is just plain wrong. I am looking to clarify my understanding of NSWindowController, so any additions (in the form of best practices, experiences, gotchas) would be highly appreciated.
Thanks,
Laurie
What are window controllers actually for?
Window controllers are tools to load a window from a NIB file and for managing the memory of the resources allocated in the NIB. Before there where NSWindowControllers one basically had to write the same code for every window or invent an own window controller class.
Of course they are also controllers in the Model/View/Controller sense, so they are the right place to connect the views from the window to the model objects. To do this they often need to act as the delegate or data source for a view object. So you got this part perfectly right.
Also window controllers are a tool for code reuse. It makes it easy to drop the window controller class and it’s XIB/NIB into another project and use it there.
So yes, every window from a NIB should be owned by a window controller, with one exception. Actually, this is just a guideline for good code, nothing enforces it.
WindowControllers and MainMenu.xib
MainMenu.xib is a different thing, there you can’t use a window controller. This NIB gets loaded by NSApplication so this has to be it’s "Files owner". There is no way to get a window controller between the NSApplication and the NIB. It also isn’t necessary to use a window controller for memory management there, since the application object lives for the entire runtime of the program, so it doesn’t have to clean up it’s resources from the NIB when it gets deallocated.
If you really need a window controller for your main window you cannot put this in the MainMenu.xib.
I hope this helps. There probably is a lot more to say about window controllers too
Is this the case even for the MainMenu.xib application?
No, the MainMenu nib is owned by NSApplication (that's who loads it).
Should this also be the case for the NSWC for the MainMenu.xib window, even though this is opened at startup?
No, NSApplication loads the main nib based on your applications file's "NSMainNibFile" property. (It just happens to be pre-set to "MainMenu" in the template Xcode projects.) If you want to change its name then change it there (and rename your nib file). (BTW: This property can also be changed in your target's "Summary" view in Xcode 4.)
Should the NSWC of the MainMenu window also be the application delegate, or should this be a different class?
The owner of the NSMainNibFile nib is the instance of NSApplication that loads it and by association any delegate of that instance. Neither of these are NSWC sub-classes.
In the same vein, should the main NSWC handle files (drag/drop and opening), or should it be passed on to the app delegate, or is that just a matter of taste?
There is no "main NSWC" (The app/app-delegate is the controller for the NSMainNibFile).
All drag-n-drop operations are handled by NSWindow or NSView sub-classes. I usually use a special NSWindow or NSView sub-class that just passes all drag-n-drop methods thru to the delegate. For example:
- (unsigned int) draggingEntered:sender
{
return [[self delegate] draggingEntered:sender];
}
This way I can keep all my window/view code together in their respective controller (as determined by their nib owner). And because the window/view specific code is in the controller (not the NSWindow/NSView subclass) different types of NSWindows/NSViews can all use the same drag-n-drop sub-classes.