Movatterモバイル変換


[0]ホーム

URL:


mikeash.com: just this guy, you know?
Home
Book
The Complete Friday Q&A, advanced topics in Mac OS X and iOS programming.
Blog
GitHub
My GitHub page, containing various open-source libraries for Mac and iOS development, and some miscellaneous projects
Glider Flying
HD Ridge Running Video
List of Landouts
Day in the Life
Skyline Soaring Club
Soaring Society of America
Getting Answers
Ten simple points to follow to get good answers on IRC, mailing lists, and other places
Miscellaneous Pages
Miscellaneous old, rarely-updated content
mike@mikeash.com
E-mail me

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:cocoadesignfridayqnawindow
Friday Q&A 2013-04-05: Windows and Window Controllers
byMike 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 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.

Did you enjoy this article? I'm selling whole books full of them! Volumes II and III are now out! They're available as ePub, PDF, print, and on iBooks and Kindle.Click here for more information.

Comments:

Anonat2013-04-05 17:01:09:
documneted -> documented
Nowe -> Now
Michaël Fortinat2013-04-05 17:13:11:
Phew! I've been doing it the right way.

Interesting read as always, keep it up!
Ari Weinsteinat2013-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.
Anonymousat2013-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 Thomasesat2013-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 Huberat2013-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 :-).
jamieat2013-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 Aufflickat2013-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 Cohenat2013-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 Odgaardat2013-04-11 19:01:16:
It’s worth mentioning that prior to 10.8 it was not possible to make a weak reference toNSWindowController 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 thatNSWindowController 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.

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.
Ianat2013-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.
Anonymousat2013-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 Haisenkoat2013-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 Haisenkoat2013-04-16 07:59:41:
s/should be called/should not be called/
Allenat2013-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.
jamieat2013-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...
HBat2013-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!

7studat2015-05-29 00:04:58:
What about cleanup under ARC?

1. The user closes the window.
2. How do you release the WindowController?
Motti Shneorat2015-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?
mikeashat2015-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 makeone nib forone 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 withNSAlert.

Don't search for problems, search for solutions.
Alexat2016-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:
The Answer to the Ultimate Question of Life, the Universe, and Everything?
Comment:
Formatting:<i> <b> <blockquote> <code>.
NOTE: Due to an increase in spam, URLs are forbidden! Please provide search terms or fragment your URLs so they don't look like URLs.
Code syntax highlighting thanks toPygments.
Hosted atDigitalOcean.

[8]ページ先頭

©2009-2025 Movatter.jp