mikeash.com: just this guy, you know?

Posted at 2013-04-05 13:36 | RSS feed (Full text feed) | Blog Index
Next article: Friday Q&A Is On Vacation
Previous article: Objective-C Literals in Serbo-Croatian
Tags: cocoa design fridayqna window
Friday Q&A 2013-04-05: Windows and Window Controllers
by Mike Ash  

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 specialized NSWindowController 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 an NSWindow instance in MainMenu.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:

    one window = one nib + one NSWindowController subclass

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 that always 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 than MainMenu.xib, should be File's Owner, which is an instance of your NSWindowController 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 the NSApplication instance, the menu bar, the application delegate, any other objects related to these, but no NSWindow instances.

How
Start off by creating an NSWindowController subclass. Give it a name like MAImportantThingWindowController to make it obvious what it is.

Next, create the nib. Give this one a name like MAImportantThingWindow.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 the NSWindowController subclass. Set the class of File's owner to the controller class. Once that's done, connect the controller's window outlet to the window, and connect the window's delegate 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. The windowDidLoad implementation it provides is useful, but doesn't contain anything interesting. The initWithWindow: method it provides is pointless and can be deleted.

NSWindowController provides a initWithWindowNibName: 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 plain init method that does the right thing internally. Simply override it to call super and provide the nib name:

    - (id)init
    {
        return [super initWithWindowNibName: @"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 this init method.

Optionally, depending on your level of paranoia, you may override initWithWindowNibName: to guard against accidentally calling it from elsewhere:

    - (id)initWithWindowNibName: (NSString *)name
    {
        NSLog(@"External clients are not allowed to call -[%@ initWithWindowNibName:] directly!", [self class]);
        [self doesNotRecognizeSelector: _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 the init method just like any other class. Note, however, that outlets are not connected at this point, so you can't do anything that involves those. UI initialization comes later.

After the nib loads, NSWindowController calls windowDidLoad, which is the perfect override point for UI initialization:

    - (void)windowDidLoad
    {
        [_myView setColor: ...];
        [_myButton setImage: ...];
    }

The implementation in NSWindowController is documented to do nothing, so it's not necessary to call super.

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
    {
        [_nameField setStringValue: 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
    {
        [self window];
        [_nameField setStringValue: 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;
        [_nameField setStringValue: _name];
    }

This also needs a line of code in windowDidLoad to sync up the UI when it does finally load:

    - (void)windowDidLoad
    {
        if(_name)
            [_nameField setStringValue: _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 = [[MAImportantThingWindowController alloc] init];

Perform any necessary setup:

    [controller setName: _name];

Then show the window:

    [controller showWindow: 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:

    [_importantThingControllers addObject: 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:

    one window = one nib + one NSWindowController subclass

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, please send it to me.

Did you enjoy this article? I'm selling a whole book full of them. It's available for iBooks and Kindle, plus a direct download in PDF and ePub format. It's also available in paper for the old-fashioned. Click here for more information.

Comments:

Anon at 2013-04-05 17:01:09:
documneted -> documented
Nowe -> Now

Michaël Fortin at 2013-04-05 17:13:11:
Phew! I've been doing it the right way.

Interesting read as always, keep it up!

Ari Weinstein at 2013-04-05 20:12:26:
Well huh. I've been writing Mac apps for a while, and I only use NSWindowController sometimes... and I often bundle a bunch of windows into MainMenu. Good points here, though, I'll have to reconsider my ways.

Steven Degutis at 2013-04-06 00:41:48:
Are there any downsides to just skipping the init methods and overriding -(NSString*)windowNibName?

Also, to avoid the problem you mentioned of the window not yet being loaded, I usually just use bindings. For example, for a simple login panel, I'll have two public NSString @properties on my NSWindowController subclass, named 'username' and 'password', and in my window I just bind the text fields to them. Bindings work beautifully for solving this problem.

Ken Thomases at 2013-04-06 06:58:21:
Thanks for writing this. I strongly concur with your thesis. Apple does a grave disservice to new Cocoa developers with their poorly constructed Xcode templates.

Peter Huber at 2013-04-06 12:31:57:
Great article and thanks: I could never think of a situation where I would actually use the default initWithWindow: call that XCode's template provides, but I always kept it around "just in case". I guess I figured that there must be a profoundly logical reason why Apple chose to provide it and that one day I would have a revelation, then go back and refactor all my old code :-).

jamie at 2013-04-08 16:58:59:
"Apple does a grave disservice to new Cocoa developers with their poorly constructed Xcode templates."

Here's a Q&A topic: How can I create my own Xcode templates for files and subclasses?

Mark Aufflick at 2013-04-09 02:15:12:
Of course the other way to deal with the property/lazy loading issue is to use bindings. In the simple example given, you can just have regular NSString properties, and then bind the NSTextField stringValue to it.

Benedict Cohen at 2013-04-09 08:44:45:
It's worth noting that "one window = one nib + one NSWindowController subclass" is conceptual similar to the default iOS approach for view controllers.

Allan Odgaard at 2013-04-11 19:01:16:
It’s worth mentioning that prior to 10.8 it was not possible to make a weak reference to 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.

Also worth mentioning that NSWindowController does not descend from NSController or implement the NSEditorRegistration protocol, this means you generally need to bind the views’ properties to an intermediate NSObjectController (where the window controller is set as the content) to have the commitEditing and discardEditing methods.

Lastly, I am surprised that you stress the need for a xib. Since embracing auto-layout I am moving more and more of my GUI from xib files into source form. The advantages of having your GUI in code are many: version controllable, easier to refactor, ability to cut, copy and paste GUI code, no need to sync class info between code and GUI editor (with a lot of run-time errors eliminated related to this), and everything is 100% explicit and searchable.

Ian at 2013-04-14 12:10:24:
@jamie: Maybe Mike will pick up the mantle and take on that topic, but I've tried making my own Xcode templates, and my experience was that it was SO painful as to not be worth it. And I even make lots of new projects (part of my normal workflow)... The cure is worse than the disease.

Steven Degutis at 2013-04-15 04:34:43:
@MikeAsh: Allan reminded me a possible Q&A topic, the NSController methods. I've never understood that class and assumed it's not really useful ever. Is this true? If not, how is it to be used?

Marc Haisenko at 2013-04-16 07:58:43:
Nice article. But I prefer a different "guard" for methods that should be called by users: the __attribute__((unavailable("A description string"))).

In your class interface, do:


- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil __attribute__((unavailable("Use 'init' instead!")));


The advantage is that the compiler issues a warning/error at compile time rather than getting an exception at runtime.

Marc Haisenko at 2013-04-16 07:59:41:
s/should be called/should not be called/

Allen at 2013-04-17 17:29:14:
Do you not recommend creating windows programmatically? I've been moving to the 1 window, 1 window controller approach but skipping the xib file.

jamie at 2013-04-27 00:04:05:
"Do you not recommend creating windows programmatically? I've been moving to the 1 window, 1 window controller approach but skipping the xib file."

I'd be curious to see examples of this, I know you can do it, but I wonder what best practices are: should you do it in the view class, in another class that "decorates" the view, do you do it at init time or show time, shortcuts for setting up bindings, etc...

HB at 2013-08-26 20:17:27:
Guess I'm a couple months late discovering this very helpful explanation of NSWindowControllers.
My code was very similar but you have some better stuff so I changed my code to match. :-)

One instance of the controller works just fine but I can't seem to have multiple instances at the same time.
The [_importantThingControllers addObject: controller];
part of your tutorial doesn't work for me. The window pops up briefly and then disappears.
Not sure what the problem is and it's getting irritating.
Help appreciated!

7stud at 2015-05-29 00:04:58:
What about cleanup under ARC?

1. The user closes the window.
2. How do you release the WindowController?

Motti Shneor at 2015-09-29 20:02:20:
What about nsalert sheet windows I need over my window? Do I need to create a separate nib for each of the hundred possible dialog alerts I have in my App?

mikeash at 2015-09-29 21:02:04:
If you create a text editor, do you create a separate nib for each of the quintillion possible documents your users might write? Of course not. You make one nib for one window which is able to contain and display arbitrary documents.

Likewise, for alerts, you'd create one nib and one window controller for a single alert window that is configurable to show the hundred possible alert messages you have in your app. And of course this is so common that Apple has already done this for you with NSAlert.

Don't search for problems, search for solutions.

Alex at 2016-11-29 22:55:00:
This is a good article, as far as it goes. It'd be great to see a similar article for setting up an NSDocument scenario. There's a lot more going on, so it's not entirely clear to me how all the pieces fit together.

And of course, NSViewController recently got a lot more powerful, and NSStoryboard is new, so I'd love to hear your ideas on how to use them, too, if at all. One-xib-per-window sounds good, but being able to set up a bunch of common operations straight from IB sounds good, too. I think there's ways to let storyboards span multiple files, but I'm not clear on how that works.

Cheers!


Comments RSS feed for this page

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.

Name:
Web site:
Comment:
Formatting: <i> <b> <blockquote> <code>. URLs are automatically hyperlinked.
Code syntax highlighting thanks to Pygments.
Hosted at DigitalOcean.