Doing Objects Right
Volume Number: 14
Issue Number: 3
Column Tag: Rhapsody
by Andrew C. Stone
Using modular objects with multiple nib files to make
evolving your projects easier
One of the most compelling features of writing software is that there are many ways to
accomplish the same task. This gives you a large latitude for creativity, but also "the
power to run off into the weeds." (I overheard an Apple Engineer using this phrase.) In
this article I present some guidelines for creating usable and reusable objects, and
provide source for a search and replace panel.
Our Rhapsody-based object draw and web authoring application, Create(tm), has 550
classes, and about 100 user-interface nib (NeXT InterfaceBuilder) files. This highly
modular structure makes changing one component trivial and speedy. Because the nib
files are loaded only when needed, it also speeds application launching.
Figure 1.
There is always a temptation to add objects directly to your main nib file because its
easy to make object connections. But this bloats the main nib and causes the app to take
longer to launch. Moreover, it makes multiple documents almost impossible because
sometimes you need more than one instance. You also may want to take advantage of
loading the nib files only when needed. This article will show you how to write an
object with its own independent interface file, and how to write the glue needed to have
a menu item bring up that interface. Code is included for a universal text find and
replace object, "TextFinder", which can be added to the simple Word Processor from
the November 1997 issue of MacTech.
sWord
The entire source of sWord, our simple rich text & graphics word
processor.
@interface WordDelegate : NSObject
- (void)newText:(id)sender;
- (void)openText:(id)sender;
- (void)saveText:(id)sender;
@end
#import "WordDelegate.h
@implementation WordDelegate
- (void)newText:(id)sender
[theText setString:@""];
}
- (void)openText:(id)sender
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
if ([openPanel runModalForTypes:[NSArray
arrayWithObjects:@"rtf",@"rtfd",NULL]]) {
[theText readRTFDFromFile:[openPanel filename]];
}
}
- (void)saveText:(id)sender
NSSavePanel *savePanel = [NSSavePanel savePanel];
[savePanel setRequiredFileType:@"rtfd"];
if ([savePanel runModal]) {
[theText writeRTFDToFile:[savePanel filename] atomically:NO];
}
}
@end
Tips and Techniques
This article won't go into style issues -- that's a topic for holy wars! However, here
are some basic guidelines for developing stand-alone objects that are truly reusable:
1. Every nib file should have an owner object to which you say "+ new:
This means that a client need know only the object's class name presenting a simple
calling interface. By separating the details of the class (such as the nib name) from its
use, you obtain a cushion from changes to the object. Then your client code looks like
this:
id aCoolObject = [CoolObject new:(NSZone *)zone];
Note that the client determines the memory allocation zone, the NSZone, in which to
create the new object by passing it as an argument. You can always pass in
"NSDefaultMallocZone()", a function which returns the default memory allocation
zone, or "[self zone]", which returns the zone of the calling object.
In our CoolObject's + new: method, we have
+ new:(NSZone *)zone
self = [[CoolObject allocWithZone:zone] init];
return self; /* don't ever forget this! */
}
In its -init method, we load the user interface file:
- init
[NSBundle loadNibNamed:@"CoolObject.nib" owner:self];
/* place initialization code here:*/
return self; /* don't ever forget this! */
}
Many objects require only one instance per class. For example, Create uses just one
TextFinder object, which brings up the same panel each time. For objects like these, it
is more appropriate to create a class method named + sharedInstance, which might
look like this:
// subclasses need their own instance if both classes are needed:
static id sharedFindObject = nil;
// get the real McCoy the first time through:
sharedFindObject = [[self allocWithZone:
[[NSApplication sharedApplication] zone]] init];
}
return sharedFindObject;
}
2. Name your nib file the same as the owner's class name
For each object which has a visual representation, your project directory will have
three associated files: the .h, .m, and .nib. (If the nib file is localized, it will reside in
English.proj, German.proj, French.proj, etc.)
If the owner's class name coincides with the nib file name, the following generic code
will load a nib file based on that class name, using the NSStringFromClass() function:
#import /* Everything you need */ - init
// Continue the designated initializer chain:
[super init];
// here's a fuller invocation of "loadNibNamed:" which shows the
loading of the
// dictionary with the key-value pair NSOwner, which has a value of
"self".
[NSBundle loadNibFile:[[NSBundle mainBundle]
pathForResource:NSStringFromClass([self class])
ofType:@"nib"]
externalNameTable:[NSDictionary
dictionaryWithObjectsAndKeys:self, @"NSOwner", nil]
withZone:[self zone]];
// place other initialization code here
return self;
}
By making our object a subclass of an object which uses this code to load a nib, we
never even have to even write a new line of code -- the nib with the name of our
subclass will be loaded automatically.
Apt class naming is one of the most important aspects of creating comprehensible, not
reprehensible, code. The name should clearly and concisely describe the object's
function. When my custom class is a subclass of an NSObject, I like to include the
superclass name in my class name. For example, SliderDualActing descends from
NSSlider. Usually, nib owners will descend from NSObject, so they can have more
succinct descriptive names, such as AlignPanel, TextFinder, or OpenAccessory.
3. Use the power of Objective C
We would like any text object to be able to use our TextFinder's search and replace
functionality, not just our own custom subclasses. Objective C allows us use categories
to add methods to existing classes. We can extend NSTextView with a category
TextFinderMethods, which contains the search and replace methods. Then any
NSTextView in our application will be able to respond to methods like findNext: or
findPrevious:.
One note of caution about categories: if you add multiple categories to a class and define
a method in more than one category, which method will be used at runtime is
undeterminable. Be sure to use categories carefully. Someday categories may be
thought of as the Object Oriented GOTO, but they reveal the power of a dynamic runtime
system. The full set of methods that we extend the NSTextView class are defined in
"NSTextViewTextFinder.m".
Objective C also provides subclassing, which allows us to reuse classes by modifying
their functionality to fit specific applications. For example, in specific text objects,
we might want to provide the capability to use regular expressions in our search
strings. We could subclass TextFinder and modify a few of its methods without having
to rework the whole object.
@implementation NSTextView(TextFinderMethods)
- (void)orderFrontFindPanel:(id)sender {
// no variable is used - instead, we grab the sharedInstance:
[[TextFinder sharedInstance] orderFrontFindPanel:sender];
}
4. Use the power of the AppKit
Your interface depends on being able to cause various controls (buttons, menu items)
to trigger actions in your code. This is easy with the TextFinder object and the
TextFinder nib file. We easily can create the necessary connections in the Interface
Builder, but now that you've followed my advice to use modular design and have created
many individual nib files, how do you connect the menu items defined in the main nib
file to targets in other nib files? How do you connect menu items for finding text to the
methods defined in the TextFinderMethods category?
The solution is the use of the AppKit's "First Responder" hierarchy.
Figure 2.
In AppKit programs, if a menu item is connected to the "First Responder" stand-in
object, then when the menu item is clicked, it sends its message up a hierarchy until it
reaches an object which responds to that method. If no object in the hierarchy
responds to that message, the menu item automatically will be disabled. Each
NSWindow in your application keeps track of which object in its view hierarchy has
first responder status. This object gets the first chance to handle messages sent to
First Responder. From there, the message is passed to the first responder's
superview, through the view hierarchy to the window and then to the window's
delegate. If the message has not yet been handled, it then goes to the NSApplication and
finally to the NSApplication's delegate.