I'm having a problem with my Cocoa app. I'm using my app delegate as a controller, and opening one window in a NIB file. Clicking a toolbar button opens another window from another NIB. Clicking save on this second window calls a method on the app delegate/controller. All this works fine.
The strange thing is that I can't figure out is that the app delegate points to one memory location when I click the toolbar button and to a different memory location after clicking save on the second window. It's as if a second app delegate/controller is being created, though stepping through the code doesn't give me any indication of that occurring.
Is there a better way to architect this type of application? Any idea of where I'm going wrong?
It sounds like you're creating a second instance of your AppController class in your window's nib file. You can't do that, each instance of an object in a nib file will be instantiated when a nib is unarchived at runtime. This means if you have an AppController instance in MainMenu.xib and also one your MyWindow.xib file, the AppController object will be alloced and initialized twice.
Normally the way you'd handle this is by using the responder chain. In your Window nib, you assign First Responder as the target of your actions. This means that when the action method is called, the app will ask the currently focused view/control (the one that has first responder status) if it responds to the method by calling the -respondsToSelector: method and passing in the action selector.
If the first responder doesn't respond to the method, the message travels up the responder chain until an object that does respond to the method is found. If no object responds to the method, the NSApplication instance handles it and calls NSBeep().
Just before the method is sent to the NSApplication instance, the application delegate is asked if it responds to the selection. In this case, if your AppController object is set as the application delegate, it will receive the message sent as the action from your object in the window nib.
If this isn't clear enough, it's worth reading the Event Handling guide
You don't have to use the responder chain. You can call a method on the application delegate by calling [[NSApp delegate] yourMethod]. You could also store a reference to the app controller by adding it as an instance variable to your NSWindowController object that loads the nib and setting it at creation time, like so:
- (id)initWithAppController:(id)aController
{
self=[super initWithWindowNibName:#"YourWindowNibName"];
if(self)
{
appController = [aController retain];
}
return self;
}
Your window controller can then call methods of your AppController directly.
Related
I'm working with an NSWindowController to implement a preferences window. Apple's documentation states that by default the controller and window aren't deallocated, because it's useful to not have to reload everything, which makes sense. But their documentation goes on to say that you can override that behavior, but not explain how.
Apple's Docs:
When a window is closed and it is part of a document-based
application, the document removes the window’s window
controller from its list of window controllers. This results
in the system deallocating the window controller and the
window, and possibly the NSDocument object itself. When a
window controller is not part of a document-based application,
closing the window does not by default result in the
deallocation of the window or window controller. This is the
desired behavior for a window controller that manages something
like an inspector; you shouldn’t have to load the nib file
again and re-create the objects the next time the user requests
the inspector.
If you want the closing of a window to make both
window and window controller go away when it isn’t
part of a document, your subclass of NSWindowController
can observe the NSWindowWillCloseNotification notification
or, as the window delegate, implement the windowWillClose: method.
I can't find anywhere that explains what to "implement" in the windowWillClose: method.
The window controller can be seen here:
https://github.com/gngrwzrd/gwpreferences/blob/master/GWPreferences/GWPreferences/GWPreferences/GWPrefsWindowController.m
Using the controller can be seen here:
https://github.com/gngrwzrd/gwpreferences/blob/master/GWPreferences/GWPreferences/GWAppDelegate.m - you can see in this code where I'm trying some bridge casting to try and force release objects but it doesn't work.
So the GWPrefsWindowController.dealloc method never gets called. Any ideas?
I understand this question is old, but for those who came here from google, the answer is quite simple.
As stated in the documentation, for non document base applications, you can simply:
Keep a reference for your NSWindowController wherever your are calling it. (In the example below it's referenced by myWindowController;
Make the class calling your NSWindowController implement the protocol NSWindowDelegate;
Release your Window Controller by setting it to nil on windowWillClose: method
To answer the question more precisely. When lazy instantiating your controller, set your class as the delegate:
-(IBAction)showMyWindowAction:(id)sender
{
// If my window controller is not nil
if (!myWindowController)
{
//instantiate it
myWindowController = [[MyWindowController alloc] initWithWindowNibName:#"myWindow"];
// set your class as delegate
[myWindowController setDelegate:self];
}
[myWindowController.window orderFront:self];
}
And then implement the windowWillClose: method from the NSWindowDelegate protocol
-(void)windowWillClose:(NSNotification *)notification
{
//Check if it's the right window that will close
if ([notification.object isEqualTo:myWindowController.window])
{
//Set your controller to nil
myWindowController = nil;
}
}
That's it, your window controller will now dealloc and since we are verifying if it's controller is nil before showing the window, everything will work!
I believe the reason why this is not implemented by default is because the initWithWindowNibName: is a somewhat heavy operation, and thus you have to think if dealloc'ing whatever is on your window will impact more or less than loading your window nib file.
I hope it helped
I have an NSWindow set up in Interface Builder. I have set the class of File's Owner to my NSWindowController and linked the window property of the controller to my NSWindow.
My controller implements NSWindowDelegate.
Now, in my controller, I have added the following:
- (void)windowDidLoad
{
[super windowDidLoad];
[self.window setDelegate:self];
}
- (void)windowDidBecomeMain:(NSNotification *)notification
{
NSLog(#"Did become main.");
}
Still, -windowDidBecomeMain: isn't called. Does anyone know why this is?
EDIT:
Trying to show a window from AppDelegate on launch. The main nib (declared in Info.plist) contains a menu item only which is linked to the AppDelegate. In the application delegate, I show an icon on the status bar and when this icon is clicked, I display the menu from the main nib.
In the application delegate, I also want to display a window which should have a window controller assigned to take care of the logic.
I believe that when this works, I will receive my window notifications.
Now, the following code doesn't show the window and I can't figure out why.
DemoWindowController *dwc = [[DemoWindowController alloc] initWithWindowNibName:#"DemoWindowController"];
[dwc showWindow:self];
Note that self is the application delegate.
I suspect your problem is due to the fact that your window controller is not actually the object that is the nibs file owner.
When you change the class in interface builder you are telling it what outlets and actions are available (which is why you are able to drag to the window outlet) but you are still responsible for passing in this object yourself.
In the case of a non-document based application, you will have a main method which calls NSApplicationMain. What this does is basically look up and load the window nib that is specified in your info.plist file and pass the current NSApplication instance to this nib as the files owner (so even though you changed the class type to NSWindowController, the object being passed in is actually of type NSApplication).
The easiest way to fix your problem is to get rid of your window controller for now (as it isn't actually doing anything yet).
You should implement the -windowDidBecomeMain: method in your app delegate. Then Ctrl+drag from your window to your appDelegate to set it as the delegate of the window to get your notifications.
Update
To answer your question regarding the WindowController beware of the following two issues:
You are creating your window controller variable (dwc) in your applicationDidFinishLaunching: method. This is released the moment you leave the method taking your window with it. Create an instance variable to hold onto the window controller instead.
Ensure that your second window nib has its file owner set to NSWindowController (or your window controller type) and that its window outlet is connected to the window in the nib file.
Your window should now display.
I have a document based cocoa application with an item in the application menu hooked up to an IBAction. Clicking the item needs to perform a task that uses an IBOutlet in the main nib file which is using another class, MyDocument. Creating 2 objects of the same class, one in each nib seems to not be working. How can I access the outlet?
Actions for menu items are often sent to the first responder so that whatever is currently selected can act on it.
It sounds like this action is something that works on the current document, then it should be implemented by the document. In this case have the menu send it's action to the first responder and then put the action method in the MyDocument class.
If the action you are trying to send is a custom one: in the Main Menu nib select the First Responder item, add your method name, then connect the menu item's selector to the action.
Read the Responders section of the Cocoa Event-Handling Guide for more info.
To summarize the above, in your NIB/XIB file, in interface builder make the connection to the First Responder object, not to Files Owner or anything else. You'll still be offered a lit of actions across potential first responders.
Cocoa then takes that selector and looks for it, starting with the NSView (if any) that's currently the first responder, then with the NSDocument that's currently in use, then with it's window controller etc etc all the way up to the Application delegate. The first object it checks that actually implements that method, it will use that object (after validating it with that same object).
So:
#interface MyDocumentTypeA : NSDocument {
}
-(void)myMenuAction:(id)sender;
-
#interface MyDocumntTypeB : NSDocument {
}
// -myMenuAction: not implemented here
-
#interface MyApplicationDelegate ... {
}
-(void)myMenuAction:(id)sender;
-
In Interface builder (or even programmatically), if you've linked the "action" of the menu item to a selector named "myMenuAction:" on the First Responder (which equates to not specifying a target when done programmatically), for the above two document subclasses the following will happen.
For MyDocumentTypeA, when the user selects that menu item, MyDocumentTypeA's -myMenuAction: will be invoked. Since MyDocumentTypeB does not implement this action, Cocoa will continue to look up the responder chain until it gets to your application delegate, which does implement it, so it will be invoked here instead.
If Cocoa finds no objects in the responder chain that implement the method, the menu item remains disabled.
There is a way how to do this, I've posted the answer in a similar thread: Access IBOutlet from other class (ObjC)
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.
How correctly to quit the Mac OS X app, when the main (the only one) closes?
I know there a method - (void)windowWillClose:(NSNotification *)notification in NSWindowDelegate. But it isn't quite suitable in my case, because it is called before NSWindow closes.
You cannot have windowDidClose event since the notification that accompanies it would be holding an invalid object (the window is likely to have been deallocated on close). To achieve what you need, make your class the delegate of the Application, and implement the following method:
- (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) theApplication;
From that method, return YES.
If your controller object has an instance in the MainMenu.nib, just make a connection from File's Owner (which means Application Object in the MainMenu.nob file). Control-Drag from File's Owner to your object, and connect the delegate outlet.
Or in source code, put something like this in your controller object's init method:
[NSApp setDelegate: self];