It's time to take a turn to some lighter fare, but to a subject that's near and dear to my heart. The fundamental UI component of a Cocoa app is the NSWindow, and there are many different ways to instantiate and manage them, but there is only one correct way: for each type of window, there should be a separate nib file, and a specializedNSWindowController subclass. I'll walk through what this means and how to do it, a topic suggested by reader Mike Shields.
Variants
It's common to see different ways of instantiating and managing windows. Xcode's templates, for example, put anNSWindow instance inMainMenu.xib, and treat the application delegate as the window's controller. It's common to pack multiple related windows into a single nib. People sometimes instantiate windows in code wherever they need them. Some will subclass NSWindow and put the controlling code in the subclass.
The central thesis of this article is that all of the approaches listed above are wrong. Yes, even the Xcode template, the first thing people see when they check out this whole Cocoa thing, is wrong. This is the fundamental correct design:
onewindow=onenib+oneNSWindowControllersubclass
Why
Fundamental separation of concerns makes this the best approach, and it ultimately costs no more time or effort than lesser approaches.
Most windows need a lot of controller functionality. Even extremely basic windows can eventually grow to take on a lot of tasks. As the largest unit of Cocoa UI, each kind of window needs and deserves its own controller class. It's possible to cram the logic for multiple windows into a single controller, but this ultimately makes no more sense than cramming the logic for a string and an array into the same class, just because you happen to be using them at the same time.
Most windows also function as independent units. It's rare to have a window thatalways appears with another window. Even if it does now, it may not later as you evolve your UI. Because of this, each window should be in its own nib file, separate from any others. The only objects in a nib, other thanMainMenu.xib, should be File's Owner, which is an instance of yourNSWindowController subclass, the window itself, and any non-window objects related to the window, such as auxiliary views and controller objects.MainMenu.xib is a special case: it should contain File's Owner, which is theNSApplication instance, the menu bar, the application delegate, any other objects related to these, butnoNSWindow instances.
How
Start off by creating anNSWindowController subclass. Give it a name likeMAImportantThingWindowController to make it obvious what it is.
Next, create the nib. Give this one a name likeMAImportantThingWindow.xib. The Xcode template for a nib with a window it in will set things up well, so you can use that. If you prefer to build it yourself, create a new empty nib file, then add a new window to it from the library.
Set up the nib with theNSWindowController subclass. Set the class of File's owner to the controller class. Once that's done, connect the controller'swindow outlet to the window, and connect the window'sdelegate outlet to the controller.
That's it for nib setup, beyond whatever specific UI you want to build yourself. It's time to make the controller aware of the nib.
Xcode will pre-populate some methods in the subclass for you, but these aren't important. ThewindowDidLoad implementation it provides is useful, but doesn't contain anything interesting. TheinitWithWindow: method it provides is pointless and can be deleted.
NSWindowController provides ainitWithWindowNibName: method. However, your subclass is built to work with only a single nib, so it's pointless to make clients specify that nib name. Instead, we'll provide a plaininit method that does the right thing internally. Simply override it to callsuper and provide the nib name:
-(id)init{return[superinitWithWindowNibName:@"MAImportantThingWindow"];}
If your window controller needs parameters to set itself up, for example a model object that it's going to display and edit, then those parameters can be added to thisinit method.
Optionally, depending on your level of paranoia, youmay override initWithWindowNibName: to guard against accidentally calling it from elsewhere:
-(id)initWithWindowNibName:(NSString*)name{NSLog(@"External clients are not allowed to call -[%@ initWithWindowNibName:] directly!",[selfclass]);[selfdoesNotRecognizeSelector:_cmd];}
I don't personally bother with this sort of guard most of the time, but it can be comforting or potentially useful to have depending on your habits and those of the people you work with.
If you have instance-specific initialization to perform, that can be done in theinit method just like any other class. Note, however, that outlets arenot connected at this point, so you can't do anything that involves those. UI initialization comes later.
After the nib loads,NSWindowController callswindowDidLoad, which is the perfect override point for UI initialization:
-(void)windowDidLoad{[_myViewsetColor:...];[_myButtonsetImage:...];}
The implementation inNSWindowController is documented to do nothing, so it's not necessary to callsuper.
NSWindowController loads its nib lazily. When initialized, it just remembers which nib it's supposed to use. Only when you ask for its window does it actually proceed to load the nib. Because of this, you need to be careful when accessing outlets in code that may run before the nib loads. For example, this method will silently fail if it's called before the window is loaded:
-(void)setName:(NSString*)name{[_nameFieldsetStringValue:name];}
There are two ways to work around this. One is to simply force the window to load before using an outlet:
-(void)setName:(NSString*)name{[selfwindow];[_nameFieldsetStringValue:name];}
This has some unnecessary overhead if the window wouldn't otherwise be loaded at this point, but works well enough. Most of the time, a window controller is being used because it's going to display the window.
The other way is to keep an instance variable for the data as well as setting it in the UI. The setter will both set the instance variable and manipulate the outlet:
-(void)setName:(NSString*)name{_name=name;[_nameFieldsetStringValue:_name];}
This also needs a line of code inwindowDidLoad to sync up the UI when it does finally load:
-(void)windowDidLoad{if(_name)[_nameFieldsetStringValue:_name];// more setup code here}
Everything else in the nib and the window controller is up to you. It all depends on what you want the window to do. At this point you can create outlets and views and controls as you normally would.
Using the Controller
With this stuff in place, using the controller is simple. First, allocate and initialize it:
MAImportantThingWindowController*controller=[[MAImportantThingWindowControlleralloc]init];
Perform any necessary setup:
[controllersetName:_name];
Then show the window:
[controllershowWindow:nil];
If there are multiple windows of this kind floating around, you usually want to add this controller to an array that holds all of the controllers of this type:
[_importantThingControllersaddObject:controller];
If there's only one, then you'll probably want an instance variable to hold it:
_importantThingController=controller;
That's it! As you use it, you may find that you need to pass more data through from whatever is instantiating and manipulating the controller. To do this, just add setters to the controller class that manipulate the UI as needed, and call those setters as appropriate.
Conclusion
There are a lot of ways to manage windows in Cocoa, but most of those ways are wrong. Sadly, there are a lot of different, incorrect techniques floating around out there, up to and including Apple's own Xcode templates. Now you know the proper way to do it. Just remember the principle:
onewindow=onenib+oneNSWindowControllersubclass
That's it for today. Check back next time for more wacky shenanigans. Friday Q&A is driven by reader ideas, so if you have a topic you'd like to see here, pleasesend it to me.
-(NSString*)windowNibName?NSWindowController objects. This is an issue when you want the window controller to be the delegate of an object in the view hierarchy (where the property should be weak). I believe that all Apple’s classes use “unsafe unretained”, so it’s only a problem with custom classes.NSWindowController does not descend fromNSController or implement theNSEditorRegistration protocol, this means you generally need to bind the views’ properties to an intermediateNSObjectController (where the window controller is set as the content) to have the commitEditing and discardEditing methods.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil __attribute__((unavailable("Use 'init' instead!")));
[_importantThingControllers addObject: controller];NSAlert.Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.