Saturday, February 9

My C4[1] Talk...

Mr. Rentczchxh has posted my talk from C4, and if you would enjoy watching a talk without paying, you can watch it. It's on hype, and how I generate it, but it also touches on other topics concerning having your own software company, like making good software, bundling, getting into stores, having sex with cylons, &c.

Watch it!

Or don't.

Labels: , , , ,

Tuesday, December 18

Transitions and Epiphanies.

It's been a crazy couple of weeks for me...

First off, Lucas Newman is leaving Delicious Monster -- as of January 1 he will be an iPhone engineer. This is an amazing opportunity for him, one I would never ask a friend to pass up. We remain buddies, although I'm running around Zoka these last couple weeks telling every girl I see that Lucas was secretly super-hot for her and is leaving now, which I think is starting to annoy him. Although, honestly, they'll probably all end up throwing themselves at him and he'll end up on top, again.

For those keeping score at home, this makes Mike Matas, Scott Maier, Tim Omernick, Drew Hamlin, and Lucas Newman that Apple has hired out of my employ. Yes, in fact, 100% of Delicious Monster's ex-employees are now working for Apple! You'd almost think Apple would start to pay me to train people for them. Oh, well. It's every kid's dream to work there, I can't say I blame them. Heck, I might work for Apple myself if they ever asked. And, like, wanted to give me EIGHTY ZILLION DOLLARS.

Also, seriously, if you want to work for Apple, you MIGHT want to, you know... GET TO KNOW ME.

--

Mike Lee is staying at Delicious Monster -- for now... DUM DUM DUM! You have to figure he's playing the various Apple teams off each other -- when you work at Delicious Monster, you don't jump for the girl that asks you to dance. Mike's all: "CoreAudio? Don't waste my time, sweetheart." "OS X Server? I'm sorry, you're not even getting an interview." "Ali Ozer and Scott Forstall got into a fistfight over me at lunch today? Now, see, these guys understand what kind of ball we are playing."

--

I realized tonight that I had yet another problem with CoreData, and it was a doozy, and not something where I could just put a hack on it. In fact, it was indicative of a fundamental architecture mismatch that I've been struggling with since I started this project.

So, this is a little vague, but I thought it might be important to document the process. Basically, when I bang up against a wall, I start looking bigger and bigger and bigger. Like, imagine I'm having trouble with a crumbling wall in an aqueduct -- my programmers brain does this: "Ok, why did I build this wall?" To keep the water in. "Why do I have water?" Because you need that to turn the water-wheel. "Is there some other way to turn it?" Not easily. "Why must it turn?" To power the grinder. "What needs grinding?" Corn. "Is there some other way to grind it?"

I'll get to truly huge things, where I start asking if the world even needs an app that catalogs books and DVDs and now boardgames when we could all be under five feet of water in a few years. Then it's time to take a nap and wake up and start again.

But my point is, you HAVE to question all the basic assumptions that led you to where you are, or you end up spending all your time writing the wrong code. I have always said that if you give me a perfectly spec'ed out program (one with a spec that can actually work, that I'm not going to have to modify as I go along), I can write that program for you in days. Always. The problem with coding is (a) fighting with frameworks, and (b) trying to figure out how the program should look, work, and interact even as we code it.

So we end up spending a lot of times fixing bugs in code that we really shouldn't have written in the first place -- code that doesn't really help the user, that just makes the app more complex, that is for a feature that never should have been put in, or is interacting with the user incorrectly and we're just putting spackle on a wall that's crumbling.

So here I am, tonight, running into my 1,000th bug with the fundamental mis-architecture in CoreData, which is that interacts with the UI layer and the disk layer / undo layer all using the same mechanism. They all rely on -didChangeValueForKey:, which is a huge mistake, because it means that, as a programmer, I can't sneak any data in -- I can't change a value without it creating an undo event.

Consider if, for example, I had a clock and its hands were CoreData objects. As they move forward through time, their position updates, so I'd tell them to update. And each time I did, an undo event would get pushed -- so the user actually could undo time.

This is obviously a contrived example, but it also points to the fundamental problem -- CoreData objects can't mix undoable and non-undoable changes.

So I've been struggling for three years now, trying to bend and hack and cajole CoreData's undo architecture into allowing me to do some actions synchronously and some asynchronously. (For instance, obviously, once the program has downloaded a cover from Amazon in a background thread, you don't want to UNDO the download -- it's not actually a state change, it's just a cache change -- yet, by default we end up with an undo event on the stack, in the MIDDLE of whatever the user is actually doing in the foreground.)

Fight fight gnash gnash complain complain. Tonight I hit on it. I needed to step back. Why isn't this working? Because undo wasn't designed this way in CoreData.

Well, I have undo in Delicious Library 1. It's not "magic" like with CoreData, but it works. In fact, now that I am thinking about it -- I've spent months and hundreds of lines of code trying to get CoreData's "magic" undo to work, when, in fact, there are really only FOUR actions that are ever undone:

1) Add a book -- undo to delete it
2) Delete a book -- undo to add it back
3) Change a property on a book, like its title or author -- undo to change it back
4) Make a loan -- undo to return the book
5) Return a book -- undo to re-make the loan

That's... about it. SO WHY HAVE I SPENT ALL THIS TIME TRYING TO GET COREDATA'S MAGIC SYSTEM TO WORK?

There's only five damn methods, at the top level, that need to participate in undo. It's pretty obvious I should be managing my OWN undoManager, turn off the one in CoreData, and just use CoreData for what it is EXTREMELY good at, which is minimal change tracking and fetching and storing data VERY VERY quickly.

Suddenly all these issues I've been having disappear. I don't have strange extra undo events on my stack when I fault in an object, because although CoreData might think my object changed, it's not driving the undo manager any more -- and when it goes to save, it's going to quickly discover there's no real substantive changes and just discard the whole event.

I don't have to try to work around some undo events by turning undo on and off, which required me to flush CoreData's transactions queue by hand, which was extremely sketchy because if you do it in some circumstances (eg, the middle of inserting a new object) the object will be corrupted.

--

I haven't started this yet -- I'll try it tomorrow. It's nice -- it'll pick up a bunch of the remaining issues I'm having in DL2, and should give us a good solid beta. The important thing here is, I was just too married to part of the code. I was so into using CoreData's magic undo that I kept going farther and farther to make it work, when I really needed to say, "Ok, this doesn't work in this situation, I'm doing my own undo in 40 lines of code."

Labels: , ,

Wednesday, May 30

Pimp My Code, Part 14: Be Inflexible!

I ask you, grasshopper, which is better: flexible code or tiny code?

"Ah," you exclaim, "Learned master, it is a trick question: code which is tiny yet flexible is best!"

WRONG! Tiny code is always best. Now you must carry water up the hill for the rest of the day.

--

What can we learn from this simple tale? Well, one thing is, I'm not very good at writing stories. But is there something deeper?

When I first started coding, way, long ago (on a PDP-11, which was essentially when I'd get eleven Pterodactyl Dinosaurs to sit down and do some Processing for me), I thought code should be flexible at all costs. If I were creating, say, a program to write a one-line message to the screens of other people logged in to the same machine, I'd write it, say, so you could plug in other kinds of screen-manipulation packages besides curses(3). Even though, you know, none would be invented until "time sharing" was something you did with condos, not processors.

The fundamental nature of coding is that our task, as programmers, is to recognize that every decision we make is a trade-off. To be a master programmer is to understand the nature of these trade-offs, and be conscious of them in everything we write.

You've probably seen some variant of this, but I'll show you my version. In coding, you have many dimensions in which you can rate code:

- Brevity of code
- Featurefulness
- Speed of execution
- Time spent coding
- Robustness
- Flexibility

Now, remember, these dimensions are all in opposition to one another. You can spend a three days writing a routine which is really beautiful AND fast, so you've gotten two of your dimensions up, but you've spent THREE DAYS, so the "time spent coding" dimension is WAY down.

So, when is this worth it? How do we make these decisions?

The answer turns out to be very sane, very simple, and also the one nobody, ever, listens to:

"START WITH BREVITY. Increase the other dimensions AS REQUIRED BY TESTING."

--

In Delicious Library 2 we have a feature where we will automatically find the libraries of your friends if they have published them. Of course, with any matching system, the trick is, how do you know this is really *my* friend named "Mike Lee", and not one of the other 17 million "Mike Lee"s around the world (that whore).

So, I came up with a basic algorithm for pulling all Delicious Libraries by the owner's name first, then disambiguating within those afterwards. One of my programmers said, "Well, what happens if we get 1,000 hits for John Smith?"

And my reply, not at all tongue-in-cheek, was, "Well, then we would be very, very rich." Seriously, if 1,000 John Smiths had registered our program, think of how many customers we'd have total: millions. Multiply that by $40, and one possible response to the problem of "too many John Smiths" would be: "Who cares, let's all move to Tahiti and spend the rest of our lives on the beach sipping rum."

I kid! Mostly. My point is, we'll have PLENTY of warning and PLENTY of resources when 1,000 John Smiths start to plague us. Note that both of these are necessary -- if I were, say, deploying just some website, and I didn't make money based on the number of people using the site, I'd be a lot more worried about it blowing up before I was ready -- there would be no guarantee that I'd have the time or resources needed to handle the problem.

In this particular case, there is a slightly slower (execution time) way to do the search that would eliminate the 1,000 John Smith problem, which I will do the day it starts to become a problem. Then I'll push a free update, and my customers will never know that it could have been an issue.

But note that, even though I *know* how to solve this problem (and increase the flexibility dimension), I'm not going to solve it now. Why? Because (a) this would kill my code brevity, (b) it would make the program run slower for everyone in the meantime, (c) it would introduce more instability, and (d) it would take a bunch of time to program and debug, so I couldn't do other, cool features.

This is really key: there's a solution out there that I know is more flexible -- many people would instantly consider this the "best" solution, and consider everything else a hack. My point, which I'll say again and again, is that there are MANY dimensions with which to evaluate any solution to a problem, and flexibility is NOT paramount.

--

When most people learn objective languages, the first thing they do is go ape. I mean, they create superclasses that have one method, which is stubbed out, and twenty children classes, each of which varies by one line of code. They fall so in love with objects that they think everything needs to be its OWN TYPE of object.

Often this is done in the name of flexibility. "Look, I have this abstract superclass which currently does the drawing for all my buttons, but you could subclass it to, say, draw 3D text!"

There is a related ailment, which is the "complete class" syndrome. Many programmers, when they create a new class, add a ton of features to that class to make it "complete" -- that is, they try to anticipate everyone who may ever use this class, and they add methods that those hypothetical users may want.

Let's say, for instance, Apple didn't have an NSArray class. So, you write your own. Great! I support you. Now, in your program you need to add objects to the end of the array and remove them from the end of the array. Ok, write those methods. But, wait, you say. Maybe I should add some more methods? Get an object from any index? Insert at the beginning? Why don't I make this more flexible, you say? NO! NO NO NO!

Now, you may be saying to yourself, "What's wrong with flexibility?" Strangely, I was about to tell you. The problem is YOU ARE NOT A LIBRARY PROGRAMMER. YOU WRITE APPLICATIONS. (Note to Ali Ozer: IGNORE THIS SECTION.)

If you find yourself writing a class for your "library," then:

(a) You're not writing your application, which is where you make your money,
(b) You're writing something that you're hoping Apple will someday replace, which is a sucker's game,
(c) You're writing code you are going to have to test SEPARATELY from your app, because BY DEFINITION you've added functionality you didn't need,
(d) You're never going to really know which methods in your library work and which ones don't (eg, which ones are used in shipping programs) because you don't have user base that a company like Apple does (and witness how buggy even their under-used frameworks are),
(e) You're writing code that is going to need documenting (or some other way to comprehend it), so you're requiring yourself and everyone at your company to understand not JUST all of Apple's APIs (which are, at least, SOMETIMES documented) but also yours, and, possibly worst of all,
(f) You are attempting to predict how your application's needs will change in the future, and spending time NOW on your guess, instead of shipping the damn application, getting feedback, and THEN making changes.

Let's look more closely at (f). It's the same old thing again, isn't it? "Don't optimize your code until after you time it" becomes "Don't make your code more flexible until after you have a plan for what your app."

--

Here are some concrete rules I enforce at Delicious Monster, now:

- We don't add code to a class unless we actually are calling that code.

- We don't make a superclass of class 'a' until AFTER we write another class 'b' that shares code with 'a' AND WORKS. Eg, first you copy your code over, and get it working, THEN you look at what's common between 'a' and 'b', and THEN you can make an abstract superclass 'c' for both of them.

- We don't make a class flexible enough to be used multiple places in the program until AFTER we have another place we need to use it.

- We don't move a class into our company-wide "Shared" repository unless it's actually used by two programs.

--

So, next time your boss tells you to "be more flexible," tell him Wil Shipley says you shouldn't. He'll probably give you a raise!

Labels:

Wednesday, December 20

Pimp My Code, Part 13: The Pimp Before Christmas, Redux

Well, it's been awhile since I've pimped anything (except other developers' moms), and frankly I feel my pimp muscles starting to atrophy. A reader was nice enough to send me some code today with the following note:

I'm sure there's a better way to do this, and there's probably some entertaining mockery to be had as well. The code's from a motion-sensing alarm thing I wrote, along the lines of iAlertU.

Mockery? I believe sir may have me confused with someone else? At any rate, let's view our victim:

Original: -awakeFromNib
- (void)awakeFromNib
{
NSRect screenRect = [[NSScreen mainScreen] frame];
NSRect boxRect = [lockBox frame];
[lockBox setFrame:NSMakeRect((screenRect.size.width - boxRect.size.width)/2,
(screenRect.size.height - boxRect.size.height)/2,boxRect.size.width, boxRect.size.height)];

NSShadow *lockShad = [NSShadow alloc];
[lockShad setShadowOffset:NSMakeSize(0,-4)];
[lockShad setShadowBlurRadius:8.0];
[lockShad setShadowColor:[NSColor colorWithDeviceWhite:0 alpha:0.75]];
NSMutableParagraphStyle *paraSty = [NSMutableParagraphStyle alloc];
[paraSty setAlignment:NSCenterTextAlignment];
shadAttrs = [[NSDictionary dictionaryWithObjectsAndKeys:[NSFont fontWithName:
@"Lucida Grande Bold" size:72],NSFontAttributeName,lockShad,NSShadowAttributeName,
[NSColor whiteColor],NSForegroundColorAttributeName,paraSty,NSParagraphStyleAttributeName,
nil] retain];
[lockShad release];
[paraSty release];
def = [NSUserDefaults standardUserDefaults];
[def registerDefaults:[NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:
[NSNumber numberWithInt:0],[NSNumber numberWithInt:5],[NSNumber numberWithFloat:0.8],
@"System locked.\nDo not touch.",nil] forKeys:[NSArray arrayWithObjects:@"compType",
@"sensitivity",@"lockedOpacity",@"lockedText",nil]]];
[self getPrefs];

// Menu icon thing
menuItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:24] retain];
menuItemIcon = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle]
pathForImageResource:@"lockicon"]];
menuItemSelIcon = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle]
pathForImageResource:@"lockiconsel"]];
[menuItem setImage:menuItemIcon];
[menuItem setAlternateImage:menuItemSelIcon];
[menuItem setHighlightMode:YES];
[menuItem setMenu:menuItemMenu];
}

There's some general clean-up to be done here, plus some of this code needs to move. First off, let's rip that defaults registration out of the middle of -awakeFromNib and into +initialize, because the latter is automatically called before any method can be called on this class, and "before everything" is a damn fine time to set up preferences.

Pimped: New +initialize method
+ (void)initialize;
{
[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary
dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:0], @"compType",
[NSNumber numberWithInt:5], @"sensitivity", [NSNumber numberWithFloat:0.8],
@"lockedOpacity", NSLocalizedString(@"System locked.\nDo not touch.", @"panel message"),
@"lockedText", nil]];
}

Note I also don't store off +standardUserDefaults in a temporary variable, because it's really fast to call this method and very self-documenting, and more variables == more clutter. And I've made the English text for the locked message localizable, which you should always do as a matter of course.

Let me say this again in slow motion: NEVER type in ANY English string without typing NSLocalizedString() around it! This will save you SO MUCH HASSLE later on when your app is popular. Remember that enterprising polyglots can localize your code from just the binary you ship if you follow a few rules of localization, so you may wake up one day and find that someone from across the world has mailed you a your app in another language. It's a fuzzy feeling and it gets you instant market-share.

Also note I'm using +dictionaryWithObjectsAndKeys: instead of +dictionaryWithObjects:forKeys:, the latter of which is obviously wordier AND harder to read and edit.

Next off, we're going to move the text attributes code out of -awakeFromNib as well, and put it into his code right before it gets used. Since I don't actually have this code, I'm just going to pretend, but imagine there's a method in which he's about to set the attributed string value of a text field... this code would be inserted right before that happened:

Pimped: Insert right before attributes are used
[...some code...]
static NSDictionary *shadowedTextAttributes = nil;
if (!shadowedTextAttributes) {
NSShadow *lockShadow = [[[NSShadow alloc] init] autorelease];
[lockShadow setShadowOffset:NSMakeSize(0,-4)];
[lockShadow setShadowBlurRadius:8.0];
[lockShadow setShadowColor:[NSColor colorWithDeviceWhite:0 alpha:0.75]];

NSMutableParagraphStyle *paragraphStyle = [[[NSMutableParagraphStyle alloc] init]
autorelease];
[paragraphStyle setAlignment:NSCenterTextAlignment];

shadowedTextAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:[NSFont
boldSystemFontOfSize:72], NSFontAttributeName, lockShadow, NSShadowAttributeName,
[NSColor whiteColor], NSForegroundColorAttributeName, paragraphStyle,
NSParagraphStyleAttributeName, nil];
}
[...actually use shadowedTextAttributes here...]
[...more code...]

Why do we do this in the main code instead of in one of the class initialization methods? First, because this offers us better locality of code -- when we see "shadowedTextAttributes" being use we don't wonder what the heck it is, and have to scroll to the top of the file to find out. It's right there. Similarly, if we want to, say, change the size of the text, we just find where we use it and the size is right there. Second, and related, we don't forget that we set up "shadowedTextAttributes" this way -- if we delete the code that uses it, we'll quickly see that we should delete the "shadowedTextAttributes" setup code as well. You would be amazed how many times I've seen classes that set up variables in their +initialize or -awakeFromNib methods that they never use any more. There's no compiler warning if the variable is declared at the top level, but there is if it's declared inline like this. Finally, our program launches faster, because we are even more lazy about initializing (or not initializing) our state until the very second we need it. The biggest performance wins you will find are always to not do any work until you absolutely need to.

I also use +boldSystemFontOfSize: instead of typing in "Lucida Grande Bold" by hand, which reduces the chances of a typo and also means our font will automatically change when Apple redoes their fonts for Leopard. (Right, Apple? Right?... Apple? Anyone?)

I'm going to remove this code, because if the NIB is set up correctly the 'lockBox' view should auto-center itself anyways. If for some reason it didn't, this is what the code would look like pimped out a bit -- note that we get the screenRect of the screen our view is actually on, instead of arbitrarily picking the main screen, and that we don't assume a screen's rect starts with 0.

Pimped: Delete this
NSRect screenRect = [[[lockBox window] screen] frame];
NSRect boxRect = [lockBox frame];
[lockBox setFrame:NSMakeRect(NSMinX(screenRect) + (NSWidth(screenRect) - NSWidth(boxRect))/2,
NSMinY(screenRect) + (NSHeight(screenRect) - NSHeight(boxRect))/2, NSWidth(boxRect),
NSWidth(boxRect)];

Finally, that leaves us with the now-gutted -awakeFromNib, which I have rewritten to not use as many temporary variables (or any at all, actually), to be less wordy when looking up images. Also, I don't know what -getPrefs does, but I'm guessing it can at least be made private:

Pimped: Final -awakeFromNib
- (void)awakeFromNib;
{
[self __getPreferences];

// Menu icon thing
menuItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:24] retain];
[menuItem setImage:[NSImage imageNamed:@"lockicon"]];
[menuItem setAlternateImage:[NSImage imageNamed:@"lockiconsel"]];
[menuItem setHighlightMode:YES];
[menuItem setMenu:menuItemMenu];
}

And that's how we pimp it up in my house, nerds and nerdettes. So, happy holidays, and don't spend all your savings snorting coke off of hoo... oh, wait, I have One More Thing!


No, it's not an Apple Phone, it's... MORE CODE. Yes, our friend actually mailed me TWO snippets of code, so we get to double our fun today.

Also, a method from another application, this one a game cache manager:
Original: -applicationWillFinishLaunching:
-(void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
// Get cache-file contents
NSString *fileCont = [NSString stringWithContentsOfFile:
[@"~/Library/Application Support/Unreal Tournament 2004/Cache/cache.ini"
stringByStandardizingPath]];
NSArray *lines = [fileCont componentsSeparatedByString:@"\n"]; // Split it up
NSEnumerator *lineEnum = [lines objectEnumerator]; // Enumerator to iterate through all
the lines
NSScanner *scanner; // String-parser scanny thingy
NSString *tmpStr; // set to full line in loop
NSString *idStr = nil; // ID string
NSString *nameStr = nil; // Name string
NSString *fileExt; // File extension
NSString *typeStr; // Type string, also folder to copy to
NSFileManager *fileMan = [NSFileManager defaultManager]; // File manager - used to get
the file attributes
NSDictionary *fattrs; // File attributes holder
cacheArray = [[NSMutableArray alloc] initWithCapacity:([lines count]-1)]; // Initialize
the main array
[lineEnum nextObject]; // Skip the "[Cache]" header
while(tmpStr = [lineEnum nextObject])
{
scanner = [NSScanner scannerWithString:tmpStr];
[scanner scanUpToString:@"=" intoString:&idStr]; // get ID
[scanner scanString:@"=" intoString:nil]; // skip the =
[scanner scanUpToString:@"" intoString:&nameStr]; // get name
fileExt = [nameStr pathExtension]; // get extension
//Figure out where it goes
if([fileExt isEqualToString:@"ut2"])
typeStr = @"Maps";
else if([fileExt isEqualToString:@"ogg"])
typeStr = @"Music";
else if([fileExt isEqualToString:@"utx"])
typeStr = @"Textures";
else if([fileExt isEqualToString:@"usx"])
typeStr = @"StaticMeshes";
else
typeStr = @"System";
nameStr = [nameStr stringByDeletingPathExtension];
fattrs = [fileMan fileAttributesAtPath:[self pathToCacheFile:idStr] traverseLink:YES];
// get file attributes
// make the final dictionary
[cacheArray addObject:[NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:
nameStr,idStr,typeStr,[[fattrs fileModificationDate] descriptionWithCalendarFormat:
@"%b %e, %Y" timeZone:nil locale:nil],fileExt,nil] forKeys:[NSArray
arrayWithObjects:@"name",@"id",@"type",@"date",@"ext",nil]]];
}
maxIndex = [cacheArray count]-1; // set the index properly
[table setDataSource:self];
[table setDelegate:self];
[table display]; // make the table update
}

Ok, let's start with the code we're going to cut, because (sing it along with me) the line of code we cut is the line of code we don't ever debug. I'm going to delete the "maxIndex" ivar (towards the end of the method), since asking an array for its count is O(1) and it's MUCH less fragile and much more readable to just use "[cacheArray count]" whenever you need that count, instead of caching an int at launch time.

Let me stress that I hate instance variables: they add needless complexity to code most of the time. Objective-C is mostly self-documenting; it's much clearer to me and any casual observer what "[cacheArray count]" is (the count of the cacheArray, maybe?) than what "maxIndex" is. DO NOT CACHE VALUES THAT ARE O(1) TO RECOMPUTE.

Also, there's a problem here, in that if there are NO lines in the file we read in, maxIndex will be set to -1 or NSNotFound (I don't know if it's unsigned or signed), and either value is likely to cause subsequent code to act anomolously (which is why we normally use 'count' instead of 'max index' for arrays and sets -- 'count' works for empty collections, and 'max index' is undefined). Not caching this count makes us face up to "cacheArray"'s possible emptiness every time we use it.

Pimped: delete this
maxIndex = [cacheArray count]-1; // set the index properly

And while I'm in a deleting mood, let's change our tableView to use bindings in NIB, so I can delete these three lines, which shouldn't exist anyways because the dataSource and delegate should be set up in NIB even if you AREN'T using bindings, which you should:

Pimped: delete this
[table setDataSource:self];
[table setDelegate:self];
[table display]; // make the table update

Ah, that's getting better.

Now let's rewrite the main method, shall we?"

Pimped: -applicationWillFinishLaunching:
-(void)applicationWillFinishLaunching:(NSNotification *)notification;
{
NSString *fileContents = [NSString stringWithContentsOfFile:[[NSString pathWithComponents:
[NSArray arrayWithObjects:[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
NSUserDomainMask, YES) lastObject], @"Unreal Tournament 2004", @"Cache", @"cache.ini",
nil]] stringByStandardizingPath]]; // Get cache-file contents

cacheArray = [[NSMutableArray alloc] init]; // Initialize the main array

NSEnumerator *lineEnumerator = [[fileContents componentsSeparatedByString:@"\n"]
objectEnumerator];
[lineEnumerator nextObject]; // Skip the "[Cache]" header
NSString *lineString; // set to full line in loop
while (lineString = [lineEnumerator nextObject]) {
// parse the line
NSScanner *scanner = [NSScanner scannerWithString:lineString];
NSString *idString = nil;
if (![scanner scanUpToString:@"=" intoString:&idString]) // get ID
break;
if (![scanner scanString:@"=" intoString:nil]) // skip the =
break;
NSString *nameString = nil;
if (![scanner scanUpToString:@"" intoString:&nameString]) // get name
break;

// figure out where it goes
NSString *pathExtension = [nameString pathExtension];
static NSDictionary *pathExtensionsToDirectoryNames = nil;
if (!pathExtensionsToDirectoryNames)
pathExtensionsToDirectoryNames = [[NSDictionary alloc] initWithObjectsAndKeys:@"Maps",
@"ut2", @"Music", @"ogg", @"Textures", @"utx", @"StaticMeshes", @"usx", nil];
NSString *typeString = [pathExtensionsToDirectoryNames objectForKey:pathExtension]; //
Type string, also folder to copy to
if (!typeString)
typeString = @"System";

NSDictionary *fileAttributes = [[NSFileManager defaultManager] fileAttributesAtPath:[self
pathToCacheFile:idString] traverseLink:YES];

NSDictionary *cacheDictionary = [NSDictionary dictionaryWithObjectsAndKeys:[nameString
stringByDeletingPathExtension], @"name", idString, @"id", typeString, @"type",
[[fileAttributes fileModificationDate] descriptionWithCalendarFormat:@"%b %e, %Y"
timeZone:nil locale:nil], @"date", pathExtension, @"ext", nil];
[cacheArray addObject:cacheDictionary];
}
}

Starting at the end, I've added "cacheDictionary" as a temporary ivar because I really want for there to be a first-class object here instead of an NSDictionary. Approximately 100% of the time that I've used NSDictionaries as a cheapie way to store data structures I've discovered myself adding code to that data in some other class, and then 100% of the time I hit my head at some point and say, "Duh, code + data = objects" and I write a class instead of using an NSDictionary and my life is a lot better. Don't make the same mistakes I did!

Also, it bothers me that we're storing NSDates in this dictionary as strings -- we're losing data in our model layer for no good reason, and forcing a particular style of internationalization in the model as well. I'd much rather we store this as an NSDate and then apply formatting in a (-gasp-) NSDateFormatter when we actually display the value. This has the double-advantage of letting us set it up in NIB. Hey, this NIB thing is like a theme with me, isn't it?

I didn't change either of those things because I don't have all the relevant code, but I'm calling them out here as something I'd want fixed from my team.

Other changes I've made include moving variable declarations down to where they are used, which we've talked about before -- it reduces the scope of variables, and having a short-lived variable is a nice step towards not having a variable, and you know how I hate variables. It also increases readability by not introducing a bunch of extraneous concepts in different places in the code.

I've also renamed variables to remove all but approved abbreviations, and after that I removed a bunch of comments which I found entirely self-evident, because I find this kind of comment worse than no comment. "nameString" is "// Name string"? No shit? Well, what's "idString"? Oh, it's the "// ID string"? I'll be damned. Wait, wait, don't tell me what "fileExtension" is, I want to guess...

I replaced a bunch of if/elses with a single dictionary lookup, which happens to be more efficient, but also makes it a lot clearer at a glance -- we're going to translate "pathExtension" into "typeString", and that happens on one line now instead of over eight or so. Now there's no chance for me to have a typo on one of the lines that I miss forever ("oops, there's a ! in one of the if statements, that condition doesn't work!").

I've added some trivial error-checking in our parsing code, so if the file is corrupted we'll bail early and not try to copy something invalid to someplace unknown. Failing nicely is always a good thing to do -- I don't know exactly what the later code does, so there may be more error-checking to be done here, but I'd like to note you don't have to always, like, create an NSError and recovery code and localized error panels to handle every stupid error condition. But you SHOULD always fail gracefully. When you are parsing anything from the disk, assume the disk hates you and wants to kill you. We have enough customers with Delicious Library now that we've actually had more than a couple of plain old cosmic-ray-zapped data files show up (eg, every 3,000th character in the file was turned into "~" or some such).

Finally, working our way back to the start of the method, I've changed the way we create the path to be more verbose. I've seen NEXTSTEP switch filesystems TOO many times to EVER type a path separator, EVER. Sure, sure, you say, Mac OS X will use "/" forever -- except I've seen this same OS run on Windows FAT32 ("\") and I've seen it run on UFS ("/") and I've seen it run on HFS (":", now translated to "/"), so I'm not buying it. Don't use "~" for the home directory, either -- don't use UNIXisms at this layer, and don't assume the application support directory is where you think it is in the directory structure. Give the Apple guys the chance to move stuff around between releases, and always use "NSSearchPathForDirectoriesInDomains()" instead.

Well, that's it for now. Have a reasonably suicide-free holiday season, if you can. Seriously, we're all miserable here -- just survive the season, and you're ahead of the game. If you get too sad, try doing something nice for somebody -- it'll make you feel good inside and, hey, you might get laid. (My personal plan is to stay drunk and happy until the sun comes out again in Feburary.)

Labels:

Thursday, October 5

Pimp My Code, Part 12: Frozen in Carbonite.

Carbon, briefly defined, is a compatibility library that ships with Mac OS X that enables older applications, written for Mac OS 9 and before, to run under Mac OS X with minimal changes (and a recompile). Carbon is descended from the original Mac Toolbox written in the early 1980s, and still shows signs of its Pascal and machine-language origins, even though it is now primarily accessed through and written in the C language.

Cocoa, briefly defined, is a new application environment that Apple got (and improved upon) when it bought NeXT in 1997. Cocoa uses a dynamic, object-oriented language (Objective-C) by design, and was the inspiration for modern languages like Java.

For a few years after the initial release of Mac OS X (10.0), there was speculation about whether Carbon or Cocoa would end up dominant. Many Mac programming groups inside and outside of Apple didn't know Cocoa and assumed it would be another flavor-of-the-month that would be quickly abandoned as unworkable, like Pink and Taligent and Copland.

Apple's official stance was initially that developers were encouraged to use either Carbon or Cocoa to write new applications; in the last year or so that message has quietly changed to just encouraging Cocoa, both because Cocoa is being enhanced faster than Carbon and because increasing numbers of Apple engineers are trying Cocoa and finding it an easier and faster to code environment within which to code.

But there are still people out there who use the Carbon APIs to program on the Mac, and there are still those who assume Carbon is "faster" and Cocoa is just "easier". While this has no basis in any timing tests I've seen, it's a myth that persists.

It should be noted that over the past eight years Apple has merged the Carbon and Cocoa runtime environments to the point where one can call Carbon routines from inside any Cocoa program, and vice-versa, so the idea that one environment is inherently "faster" is kind of crazy, although one can obviously fail to optimize one's code, and there is a partly-relevant question which goes: if you write the same number of lines of code in each language to do the same job, which would run faster?

The answer, in my experience, is Cocoa wins hands-down, because there's no such thing as a Carbon program that's the same number of lines as a Cocoa program that does the same thing. Carbon is wordy, in part because it is based on C, and in part because it still uses metaphors and programming conventions that were in vogue twenty to thirty years ago.

--

What's distressing to Cocoa programmers is that there are still critically important Apple APIs that are only available through the incredibly byzantine and ill-documented Carbon libraries, and some groups at Apple are still generating Carbon code, under the guise of "Core".

Now, let me be clear. I have nothing against "real" Core libraries, like CoreFoundation or CoreGraphics or CoreAnimation. These are all modern APIs that happen to be written in C, and they tend to have good to great support above them in Objective-C, either via separate APIs (like NSBezierPath in AppKit, which automatically emits the correct CoreGraphics calls) or via toll-free bridging (like NSStreams and CFStreams).

But some "Core" libraries aren't "Core" at all -- they are still using the conventions and structures of Carbon, and these libraries are nigh-impossible to use. QuickTime, Keychain / Security Services, Core Audio, Launch Services, Speech and Voice recognition -- these are the ones I've recently run up against. The problems with even well-written Carbon libraries are myriad:
  1. There is usually one, giant, all-encompassing "setAttributes" function and one "getAttributes" function for setting and getting every value associated with a Carbon "object", which requires to you laboriously build up and then laboriously unparse huge, special parameter structures for even the simplest call. For example, see one of the newer (v6.4) QuickTime calls, for setting parameters on a FireWire video camera: VDIIDCSetFeatures(VideoDigitizerComponent ci, QTAtomContainer *container). That one call is used to set a HUGE number of parameters, which is "simple," except, oh, it takes eighty lines of code to prepare for that one call, and the parameters you can set are not actually documented in the documentation. (This is not an exaggeration, I'll show Apple's own example code later.)

  2. Carbon calls with monolithic parameter blocks and anonymous parameters (like the one above) are not inherently self-documenting, and the documentation for them is often wrong and/or incomplete because the documenters' job is so much harder. For example, with the given documentation there's really no way you could intuit how to build up a legal parameter block to actually call VDIIDCSetFeatures() unless you also read the technote Apple wrote on it in 2005, which is very helpful, but was written a year too late for Delicious Library and several years after QuickTime 6.4 was released.

  3. Parameters passed into functions are passed by reference, whether they are going to be modified or not, which is just obscure. For example, see SecKeychainItemCopyAttributesAndData(), which takes the input "info" by reference, even though it's read-only!

  4. Carbon uses the same type, OSTypes (eg, 'sit!'), extensively for everything: return codes, building parameter blocks (specifying what you want to do, how you want to do it), loading QuickTime components, etc., which means reading a function description doesn't actually give you enough information to know what to actually pass it. (Eg: "Pass it some combination of four-character constants. Good luck!")

  5. Carbon memory management is a mish-mash of new and old metaphors, so you might easily find yourself using, in addition to CoreFoundation objects that can be retained and released, "Handles" or "Atoms", which are obtuse and easy to screw up.

  6. Carbon often uses FSRefs (although there are now often string-path equivalents) to refer to files, which are, again, not fun or easy to build or use.
Let me note that I'm not trying to slam Apple's Carbon programmers here; if I were a programmer on some Carbon toolkit I wouldn't necessarily try to break every metaphor everyone else was using, either -- I'd add my code and clean things up a bit where I could. (Or, actually, I would, but I would suggest we just rewrite everything in Cocoa, which is what I'm getting to in this post.)

Carbon has matured a lot in the last several years (eg, more Cocoa-like). But it's not enough. What I want to do, with this post, is encourage Apple to finally move those necessary but neglected frameworks all the way to Cocoa.

--

Ok, so I've been a bit abstract. Let's look at two function calls from two different Carbon libraries that are under active development and that you MUST use if you want their functionality -- there is not equivalent in any other framework. We'll take them apart, see why they are so hard to use, and then look at an ideal Cocoa API that would accomplish the same thing but be a zillion times easier to call and understand.

First off, let's look at how you might set the hue, saturation, sharpness, brightness, gain, iris, shutter, white balance, gamma, temperature, zoom, focus, pan, tilt, optical filter, focus point, and more on IIDC cameras.

Ok, wait, wait a minute. What did I just say? "IIDC cameras"? What the hell is an IIDC camera? And why do I care? Doesn't QuickTime isolate me from the low-level nonsense and just provide me with an opaque Sequence Grabber object which handles all the hardware bullshit and provides me with a single unified interface?

Well, the answer is, "Of cou--no. No, not really. Psych."

If you have a certain class of FireWire cameras which have a common way of setting their parameters, Apple has provided low-level calls to set those parameters that only work with those cameras, and deprecated the other function calls. Note that, if you have an older (or newer) camera, or a USB camera, like the internal USB iSight that ships with EVERY iMac and notebook Apple sells, these IIDC functions don't work for you, and you have to use the older functions (where available), even though Apple says they are deprecated. Wheee!

Ok, so assume you have one of these cameras, and you want to set, say, the gain on it. How would you do it? Well, normally, you'd spend months guessing at how to build up the parameter block to please VDIIDCSetFeatures(), but as of 2005, as I said, Apple has a technote, which shows how the gain can be set, in only eighty lines of code.

Seriously. Eighty. Let's look:

Setting the Gain on a Camera in QuickTime
ComponentResult ConfigureGain(SGChannel inChannel)
{
QTAtomContainer atomContainer;
QTAtom featureAtom;
VDIIDCFeatureSettings settings;
VideoDigitizerComponent vd;
ComponentDescription desc;
ComponentResult result = paramErr;

if (NULL == inChannel) goto bail;

// get the digitizer and make sure it's legit
vd = SGGetVideoDigitizerComponent(inChannel);
if (NULL == vd) goto bail;

GetComponentInfo((Component)vd, &desc, NULL, NULL, NULL);
if (vdSubtypeIIDC != desc.componentSubType) goto bail;

// *** now do the real work ***

// return the gain feature in an atom container
result = VDIIDCGetFeaturesForSpecifier(vd, vdIIDCFeatureGain, &atomContainer);
if (noErr == result) {

// find the feature atom
featureAtom = QTFindChildByIndex(atomContainer, kParentAtomIsContainer,
vdIIDCAtomTypeFeature, 1, NULL);
if (0 == featureAtom) { result = cannotFindAtomErr; goto bail; }

// find the gain settings from the feature atom and copy the data
// into our settings
result = QTCopyAtomDataToPtr(atomContainer,
QTFindChildByID(atomContainer, featureAtom,
vdIIDCAtomTypeFeatureSettings,
vdIIDCAtomIDFeatureSettings, NULL),
true, sizeof(settings), &settings, NULL);
if (noErr == result) {
/* When indicating capabilities, the flag being set indicates that the
feature can be put into the given state.
When indicating/setting state, the flag represents the current/desired
state. Note that certain combinations of flags are valid for capabilities
(i.e. vdIIDCFeatureFlagOn | vdIIDCFeatureFlagOff) but are mutually
exclusive for state.
*/
// is the setting supported?
if (settings.capabilities.flags & (vdIIDCFeatureFlagOn |
vdIIDCFeatureFlagManual |
vdIIDCFeatureFlagRawControl)) {
// set state flags
settings.state.flags = (vdIIDCFeatureFlagOn |
vdIIDCFeatureFlagManual |
vdIIDCFeatureFlagRawControl);

// set value - will either be 500 or the max value supported by
// the camera represented in a float between 0 and 1.0
settings.state.value = (1.0 / settings.capabilities.rawMaximum) *
((settings.capabilities.rawMaximum > 500) ? 500 :
settings.capabilities.rawMaximum);

// store the result back in the container
result = QTSetAtomData(atomContainer,
QTFindChildByID(atomContainer, featureAtom,
vdIIDCAtomTypeFeatureSettings,
vdIIDCAtomIDFeatureSettings, NULL),
sizeof(settings), &settings);
if (noErr == result) {
// set it on the device
result = VDIIDCSetFeatures(vd, atomContainer);
}
} else {
// can't do it!
result = featureUnsupported;
}
}
}

bail:
return result;
}

Wow, easy!

Remember, this is only for a certain class of cameras (including external iSights, but excluding internal iSights) -- if you want your code to work in all cases, you also have to call SGSetSettings() (or some such, I honestly don't know, as the focus calls I was trying to do don't have equivalents) to set up other kinds of cameras, except this call isn't as flexible, and, of course, nowhere in the documentation for the call does it say which settings you can actually set. Because, since everything in Carbon just takes and returns OSTypes (see sin #4), it's not self-documenting what kinds of parameters you can pass in to any method -- there are tens of THOUSANDS of OSTypes defined in the system, good luck finding the list of the ones your particular function takes and returns! They certainly are never listed in the documentation of the functions that take them.

Now, if Apple had used an enumerated type instead of an OSType, as such:
Imaginary QuickTime C Typdef
typedef enum {
kSequenceGrabberFocus, kSequenceGrabberZoom, kSequenceGrabberGamma,
[...]
} SequenceGrabberParameters;

Well, then we could just command-double-click on the function prototype in XCode, and we'd have our own documentation (after a fashion), in the form of the header file where the parameters are defined.

(On a side note, sadly, the AppKit and Foundation team just announced they are moving AWAY from using enumerated types in Cocoa methods and switching everything to take plain ints, because they HATE self-documenting code and hate Cocoa programmers. No, it's because they are worried that enumerated types could have unspecified sizes. But, seriously, Ali, this is totally broken -- enumerated types make reading and writing code MUCH easier, and they enforce sanity in switch() statements and if() comparisons. Please don't break this! [Update: actually, it's not quite as bad as it could be: the proposed new types aren't totally anonymous, they just are typedef'ed ints, but at least they are defined right below real enums, so command-double-click still works. Debugging and switch() and if () still aren't handled as nicely, though.]

Now, the above enumerated type would be helpful IF QuickTime had to stay with the giant, all-encompassing set-get functions. But must they? Well, no. For some reason some Carbon programmers think it's easier for them to add and remove APIs if they never commit to any particular functionality, so they use those giant anonymous set-get functions and then just have you build up a list of parameters yourself, with the idea being that it's a lot more flexible if they change the parameters out from under you than if they change the functions.

Acrazypersonsezwhat? Ok, I don't know what life was like under Mac OS 9, but nowadays we've got a well-defined system for deprecating APIs, and it works. And the advantages to not building up parameter blocks are huge. To wit, imagine the above gain-setting code, rewritten in a (hypothetical) Cocoa interface to QuickTime:

Setting the Gain on a Camera in Imaginary Cocoa QuickTime
ComponentResult ConfigureGain(QTSequenceGrabblerChannel *sequenceGrabblerChannel)
{
return [sequenceGrabblerChannel setGain:1.0];
}

Sure, four lines is less than eighty, but the observant amongst you will say, "Wait a minute, in the original code he checks to see if this is an IIDC camera first!" Well, yes, but, honestly, I should be able to call my mythical -setGain: on any physical sequence grabber, and if the user's particular hardware doesn't implement gain, QT should just ignore me.

More observant folks may exclaim, "But, he sets the camera's gain feature to be: on, manual, and raw control!" And I must calmly say, if I call -setGain: on a sequence grabber, it's implicit that I am putting it under manual control! Otherwise, what does setting the gain mean? I should not have to write this code.

Even more observant folks may add, "Well, but, he also has some crazy code where he either sets the value to the camera's raw maximum or 500..." to which I respond: I should be abstracted away from such nonsense. Parameter values should be normalized from 0.0 to 1.0 for me; where 1.0 is the hardware maximum, whatever it is. I should never have to query the hardware.

Now, it could be I'm oversimplifying a bit. I can imagine there are complexities in this code that require, say, TWO lines of Cocoa inside my imaginary rewrite of ConfigureGain(), not just one. But you can see where I'm going with this.

The fact that Apple had to write EIGHTY lines of example code to show how to set A SINGLE PARAMETER on SOME TYPES of cameras to its maximum value shows that THESE APIS ARE SERIOUSLY BROKEN. If you ever find yourself saying, "Look, to use these APIs I've written, you just have to write four tons of this glue code which never changes..." just stop! Write the damn glue code yourself, and provide a higher-level API!

--

You might hope that QuickTime for Leopard, which has been publicly announced to be at least partly rewritten with Cocoa APIs, will solve these problems. I certainly hope so. But, historically, some Carbon teams, when faced with writing Cocoa code, take a simple and not-very-helpful route -- they provide only one or two methods in their Cocoa classes, when their Carbon APIs are incredibly rich and feature-ful. The first iteration of QuickTime in Cocoa was like this -- look at NSMovie, which has TWO whole methods, "-URL" and "-QTMovie".

Wow! I'm overwhelmed! I mean, it's pretty clear how I would add a track, or record from a camera to a movie, or save to disk, or... Oh, wait, no, I can't do any of those things from Cocoa. I've essentially been given a busy-box. "Here, you're a Cocoa programmer, you can't handle actual functionality... just put your little movie in your little view in your little NIB and be happy."

But, wait, my scorn is too quick. Look what we got in 10.4, to replace NSMovie: QTMovie, which seems pretty darn rich from where I'm standing. At least, it has more than two methods, so I think that's a big improvement.

Or let's take NSSpeechRecognizer, written by a new friend of mine from WWDC, whom I'm sorry to single out this way.

Now, NSSpeechRecognizer has a nice interface. It's clear how to use a NSSpeechRecognizer, and it's easy as heck to integrate into your applications. I put speech recognition in Delicious Library 1 in about a day using this class, and most of the work was in building up a sane list of words to recognize and keeping it up-to-date without too much overhead.

But if you only speak Cocoa, you'll think that all Apple's speech recognition can do is what NSSpeechRecognizer does: recognize a flat list of words and phrases. You'd assume you can't recognize a phrase from one list followed immediately by a phrase from another list, much less set up a tree of expected phrases. And certainly there's no way to turn someone's speech into a list of phonemes for further processing, or to record and analyze the actual tones the user uttered. Heck, there are only four attribute set/get methods in NSSpeechRecognizer, so that must be about it for Apple's code, huh?

Well, as you've guessed by now, no. I'm told, by the original programmers, that all the above functionality is in the Carbon APIs. However, I have no idea, because I simply don't have the patience to learn or program raw Carbon unless I'm forced to, and in this case I wasn't.

There are a ton of neat applications I could make if NSSpeechRecognizer (and its friend, NSSpeechSynthesizer) were fully Cocoa. It's too bad.

--

Ok, let's pick on another Carbon function call, and then invent another Cocoa API that should exist but doesn't.

This time, it's from Keychain. Now, Keychain is massively cool; all your passwords, from all your programs, can be stored securely in a central (encrypted) location, so you don't have to remember them -- all you have to do is remember the password to your keychain. You get to set the policy on which programs can access which passwords and how. Keychain is full-featured and awesome...

...aaaaaaaaaaaaand it's Carbon. So, it brings all that baggage with it, which is really sad.

Let's look at some code I wrote recently to build up a list of FTP sites that are currently stored in the user's keychain, so I can automatically suggest a place to upload a user's Delicious Library in 2.0:

Build List of FTP Sites Using Carbon Keychain
#define HANDLE_ERROR(returnCode)if (returnCode != noErr) NSLog(@"keychain error %d encountered on line %d of file %s", returnCode, __LINE__, __FILE__);

- (NSArray *)allFTPSiteDictionariesFromKeychain;
{
NSMutableArray *mutableSites = [NSMutableArray array];

[mutableSites addObjectsFromArray:[self _ftpSitesWithProtocol:kSecProtocolTypeFTPAccount]];
[mutableSites addObjectsFromArray:[self _ftpSitesWithProtocol:kSecProtocolTypeFTP]];
[mutableSites addObjectsFromArray:[self _ftpSitesWithProtocol:kSecProtocolTypeFTPS]];
[mutableSites addObjectsFromArray:[self _ftpSitesWithProtocol:kSecProtocolTypeFTPProxy]];

return mutableSites;
}

- (NSArray *)_ftpSitesWithProtocol:(SecProtocolType)protocol;
{
NSMutableArray *mutableSites = [NSMutableArray array];

SecKeychainAttribute findInternetPasswordsAttributes[] = {
{kSecProtocolItemAttr, sizeof(protocol), &protocol},
};
SecKeychainAttributeList findInternetPasswordsAttributeList = {
sizeof(findInternetPasswordsAttributes) /
sizeof(*findInternetPasswordsAttributes), findInternetPasswordsAttributes
};

SecKeychainSearchRef searchRef = NULL;
OSStatus returnCode = SecKeychainSearchCreateFromAttributes(NULL,
kSecInternetPasswordItemClass, &findInternetPasswordsAttributeList,
&searchRef);
HANDLE_ERROR(returnCode);
if (!searchRef)
return nil;

do {
SecKeychainItemRef internetPasswordKeychainItemRef = NULL;
returnCode = SecKeychainSearchCopyNext(searchRef,
&internetPasswordKeychainItemRef);
if (internetPasswordKeychainItemRef == NULL || returnCode ==
errKCItemNotFound)
break;
HANDLE_ERROR(returnCode);

#define HACK_FOR_LABEL (7)
SecItemAttr itemAttributes[] = {kSecDescriptionItemAttr, kSecTypeItemAttr,
kSecCommentItemAttr, kSecProtocolItemAttr, kSecAccountItemAttr,
kSecServerItemAttr, kSecPathItemAttr, kSecSecurityDomainItemAttr,
kSecCreatorItemAttr, kSecTypeItemAttr, HACK_FOR_LABEL}; // RADAR
3425797 - You can't get the 'kSecLabelItemAttr' using this API
(it crashes), so either we have to use the number '7' or use
SecKeychainItemCopyContent(). I do the former. See
http://lists.apple.com/archives/Apple-cdsa/2006/May/msg00037.html
SecExternalFormat externalFormats[] = {kSecFormatUnknown, kSecFormatUnknown,
kSecFormatUnknown, kSecFormatUnknown, kSecFormatUnknown, kSecFormatUnknown,
kSecFormatUnknown, kSecFormatUnknown, kSecFormatUnknown, kSecFormatUnknown,
kSecFormatUnknown};
NSAssert(sizeof(itemAttributes) / sizeof(*itemAttributes) ==
sizeof(externalFormats) / sizeof(*externalFormats),
@"arrays must have identical counts");
SecKeychainAttributeInfo info = {sizeof(itemAttributes) /
sizeof(*itemAttributes), (void *)&itemAttributes,
(void *)&externalFormats};

SecKeychainAttributeList *internetPasswordAttributeList = NULL;
returnCode = SecKeychainItemCopyAttributesAndData(internetPasswordKeychainItemRef,
&info, NULL, &internetPasswordAttributeList, NULL, NULL);
HANDLE_ERROR(returnCode);
if (internetPasswordAttributeList) {
NSMutableDictionary *siteDictionary = [NSMutableDictionary dictionary];
unsigned int attributeIndex;
for (attributeIndex = 0; attributeIndex <
internetPasswordAttributeList->count; attributeIndex++) {
OSType tag = internetPasswordAttributeList->attr[attributeIndex].tag;
if (tag == HACK_FOR_LABEL)
tag = kSecLabelItemAttr;

NSString *tagString = [NSString stringForOSType:tag];
id value;
if (tag == kSecPortItemAttr)
value = [NSNumber numberWithUnsignedInt:*(unsigned int *)
&(internetPasswordAttributeList->attr[attributeIndex].data)];
else if (tag == kSecCreatorItemAttr || tag == kSecProtocolItemAttr)
value = [NSString stringForOSType:
(OSType)internetPasswordAttributeList->attr[attributeIndex].data];
else
value = [[[NSString alloc] initWithBytes:
internetPasswordAttributeList->attr[attributeIndex].data
length:internetPasswordAttributeList->attr[attributeIndex].length
encoding:NSUTF8StringEncoding] autorelease];
[siteDictionary setObject:value forKey:tagString];
}
if (IsEmpty([siteDictionary objectForKey:[NSString
stringForOSType:kSecLabelItemAttr]]))
[siteDictionary setObject:[siteDictionary objectForKey:[NSString
stringForOSType:kSecServerItemAttr]] forKey:[NSString
stringForOSType:kSecLabelItemAttr]];
[siteDictionary setObject:(id)internetPasswordKeychainItemRef
forKey:@"keychainItem"];

[mutableSites addObject:siteDictionary];

SecKeychainItemFreeAttributesAndData(internetPasswordAttributeList,
NULL);
}
CFRelease(internetPasswordKeychainItemRef);
} while (1);

CFRelease(searchRef);

return mutableSites;
}

Now, as I said, Keychain doesn't have the worst interface in the world. It's really pretty good for Carbon -- you build the equivalent of an NSEnumerator with SecKeychainSearchCreateFromAttributes() (albeit without the nice generality of the former class), enumerate through it with SecKeychainSearchCopyNext(), and then get the data you want from the item in SecKeychainItemCopyAttributesAndData(). But at this point in this marathon post (aren't you sorry you picked on me for not posting in so long) you're an old Carbon hand, so you can spot the sins Carbon brings with it.

But, just in case, let's go over them:
  • We have to build up a bunch of structures by hand so we can build up a "search specifier" in a single parameter in SecKeychainSearchCreateFromAttributes(), which hurts our readability a ton -- we really want for each statement we write to read like a sentence, almost, and if we have to write four lines of "struct this" and "allocate that" before we get to the verb, we've got pretty awkward sentences.

  • We loop over the items we find using "SecKeychainSearchCopyNext()" (nice), but we have to manage memory ourselves in Carbon; there's no autorelease (or garbage collection, heaven forfend!) so we have to free every returned value explicitly.

  • The returned object is an array of structures of structures of void pointers to things with counters and stuff and things -- ok, I've totally forgotten, honestly. But, my point is, it's not just an NSDictionary full of nice objects (or a CFDictionaryRef), so I have to parse out the values by hand, INCLUDING byte-swapping on Intel machines. Whee! I love it!

  • SecKeychainItemCopyAttributesAndData() has a crasher if you ask to retrieve the most common attribute you'd ever ask for from a Keychain item: the label. I should point out that this function is documented to deprecate the older method of getting data from an item, which was more obtuse but didn't crash.

    Now, ok, I write crashers too. I don't blame the Apple engineers for that, and it was really nice of them to step up and provide a workaround. Because of their help, this only delayed me by a couple hours instead of a couple days. My point here is, if this whole framework were a lot more easier to use (-cough-Cocoa-cough-) then it'd get used a whole lot more, and this bug would have been found LONG ago. The problem here isn't that a bug was written, it's that an incredibly valuable framework is needlessly crippled by being in Carbon and thus not used enough to be as robust as it should be.

  • SecKeychainItemCopyAttributesAndData() isn't actually documented correctly, so the only way to write code for it is to get on the web and look at the source code and read Apple's mailing lists. In particular, the docs say for "attrList": "On input, the list of attributes in this item to get..." which is, frankly, just false. A look at the source code (search for "ItemImpl::getAttributesAndData(") clearly shows "attrList" is ignored on input, which only makes sense, because it's an output structure and you've already passed in the list of attributes to get in "info", so why would you pass it in twice? Note also the docs say that "itemClass" is "A pointer to the item’s class" but don't say if you are supposed to pass it in, or if it gets returned to you? (It turns out to be the latter). And "outData" is described opaquely as "A pointer to a buffer containing the data in this item." Oh, the data, you say? Well, that's certainly specific! I'm always getting and setting data on my items.

    It turns out that data in this case means the actual encrypted password. So, in summary, what the documentation for this function doesn't say, but should, is, "You build up a list of parameters you want to fetch from itemRef in info, and this method returns their values in attrList if that's not passed in NULL. The item's class (here there'd be a link to all the classes) is returned by reference in itemClass if it's not passed in NULL, and the password is decrypted from the keychain into outData if that is not passed in NULL."
Ok, we've rewritten the documentation, now let's rewrite my original method to use a hypothetical Cocoa Keychain framework. Note that this rewrite is longer than the last because the base Keychain APIs are more recent than the QuickTime ones I pimped above, so they already use more modern metaphors and are thus already somewhat efficient. But can we do better? (Hint: yes.)
Build List of FTP Sites Using Hypothetical Cocoa Keychain
- (NSArray *)allFTPSiteDictionariesFromKeychain;
{
NSMutableArray *mutableSites = [NSMutableArray array];

[mutableSites addObjectsFromArray:[self _ftpSitesWithProtocol:NSKeychainSecurityProtocolTypeFTPAccount]];
[mutableSites addObjectsFromArray:[self _ftpSitesWithProtocol:NSKeychainSecurityProtocolTypeFTP]];
[mutableSites addObjectsFromArray:[self _ftpSitesWithProtocol:NSKeychainSecurityProtocolTypeFTPS]];
[mutableSites addObjectsFromArray:[self _ftpSitesWithProtocol:NSKeychainSecurityProtocolTypeFTPProxy]];


return mutableSites;
}

- (NSArray *)_ftpSitesWithProtocol:(NSString *)protocol;
{
NSKeychainFetchRequest *keychainFetchRequest = [[[NSKeychainFetchRequest alloc] init] autorelease];
[keychainFetchRequest setPredicate:[NSComparisonPredicate
predicateWithLeftExpression:[NSExpression expressionForKeyPath:NSKeychainSecurityProtocol]
rightExpression:[NSExpression expressionForConstantValue:protocol]
modifier:NSDirectPredicateModifier type:NSEqualToPredicateOperatorType options:0]];
NSError *error = nil;
NSArray *matchingKeychainItems = [[NSKeychain defaultKeychain]
executeFetchRequest:keychainFetchRequest error:&error];

if (error) {
NSLog(@"%@: Error doing keychainFetchRequest '%@'", keychainFetchRequest);
return nil;
}

return matchingKeychainItems;
}

_ftpSitesWithProtocol got so tiny because, assuming I didn't have to access the returned keychain results as raw data -- that is, if there were real, full-fledged NSKeychainItem objects that could be queried for their -protocol and -account and -password and -label, then I wouldn't need to do all the complicated object-building I did in the above example, I'd just return the actual keychain items, and even bind to them in my interface directly! This would be pretty rocking... you can imagine feeding an NSArrayController from this list and then binding columns in a tableView to "label" or what-have-you, and you've got a poor man's Keychain Access app in like five lines of code.

Now, again, this might be an oversimplification of Keychain's APIs. There may be subtleties that I'm missing that would require a slightly more complicated set of Cocoa classes. But in general, I think porting Keychain to Cocoa would decrease the amount of effort to program to it (and increase its audience) by an order of magnitude.

--

Let me wrap up, finally, by apologizing profusely to everyone at Apple and elsewhere whom I've insulted tonight. I've been a programmer for 25 years now, and every year I look back at the code I wrote the year before and think, "What the hell? What idiot-monkey wrote this crap?"

And yet, here I sit, on my medium-size pony, and insult code that some of you wrote ten years ago, viewing it through the lens of modern coding practices that you guys taught me. Let me stress this: the good programming practices I've learned, such that I've learned them, are from studying the very best of what Apple's done. There are tools and techniques that I love and am in awe of in Apple's code: bindings, enumerators, object-oriented database access, model-view-controller, all those buzzwords. I didn't invent 'em, you guys did.

I know that Apple people exist in the real world, where going back and rewriting blecherous code is not something any engineer wants to do, nor any manager to fund. I know if you meet with Steve in six months and say, "Hey, sure, we didn't add anything cool in Leopard for you to demo, but check it: Keychain is Cocoa now! Huh? Huh?" he'd probably put his foot so far... well, you know where I'm going with this.

And I know that it's a small miracle that things like QuickTime and Keychain and Speech Synthesis and Core Audio exist in the first place. These have truly amazing functionality, using research and math far beyond my ken: if they weren't, I honestly wouldn't give a crap that all their power is frozen in Carbonite right now. (You don't see me complaining about the, uh, Scrap Manager, do you?)

So, I write this to help you, putative Carbon-coding-Apple-engineer-reader, not to harm you. I want you to take this to your manager, and say, "Look, see! We've got to get to Cocoa! This has got to be a priority! We can't keep slapping tar on a boat made out of milk cartons!"

Don't do it for me, do it for your team. Do it because you'll love it, and because more programmers will use your stuff, and they'll show it off in ways you never imagined, and suddenly you'll be in all the Stevenotes, and you'll be wealthy and happy and famous.

Labels:

Monday, July 3

Pimp My Code, Part 11: This Sheet is Tight

Today's post is a meta-pimp -- this is the summary of some guidance I gave one of my programmers here at Delicious Monster. Most of this isn't new information; it should in fact be standard practice for experienced Cocoa hands. Consider this a remedial pimping; for young pimps and the pimp-at-heart.



Most sheets you'll pop up will be "alert sheets," of the simple "Hey, uh, you're about to do something bad and/or extremely bad, are you sure you want to continue?" variety. (The examples here are all cribbed from Apple's excellent user interface guidelines, which you should read and re-read.)

Alert sheets are simple to program, although not as simply as alert panels used to be under Rhapsody (pre-10.0), because panels are application-modal and sheets are document-modal. The relevant difference for users between the two modalities is that with document-modality the user is still allowed to mess around with other documents, whereas in app-modal mode you put your panel in front of everything and force the user to vote yea or nay before she does anything else, period.



Contrast this with Mac OS 9's largely system-modal panels, where, for example, if you were printing in one application the print panel would block the user from interacting with any other applications until the print was complete. This, frankly, sucked from the user. Using induction we can deduce that finer-grained modality is preferable, and thus that we should use document-modality over app-modality, even though app-modality still exists in the Cocoa APIs. And, for example, iTunes uses application-modal panels on their info panels instead of, say, document-modal sheets, which is really pretty bletcherous. (Really, they shouldn't use modes at all in an info panel, but that's a separate issue.)

So, before 10.0, you would find yourself asking the user's permission (or forgiveness) using one of the following two panel functions. You can still use them if you come up with a really good excuse to block the entire application -- for example, if the user selects a menu option like, "Delete this application and add my name to a list of people who are never allowed to run it again":

int NSRunInformationalAlertPanel(NSString *title, NSString *msg, NSString *defaultButton, NSString *alternateButton, NSString *otherButton, ...);
int NSRunCriticalAlertPanel(NSString *title, NSString *msg, NSString *defaultButton, NSString *alternateButton, NSString *otherButton, ...);

Note that there are two nice variants provided here, both stolen from Mac OS 9 by NeXT engineers a long, long time ago -- the "informational" panel and the "critical" panel. For your reference, here's a friendly quote from the currently-current Apple HIG guidelines explaining when to use which: "In rare cases, you may want to display a caution icon in your alert, badged with the application icon as shown in Figure 13-30. A badged alert is appropriate only if the user is performing a task, such as installing software, and a possible side effect of that task would be the inadvertent destruction of data."

And here's an example of a critical alert panel:



Notice that there's a big yellow exclamation point, which is the international symbol for "you are going to lose data." (Not really, I just made that up.) As a programmer, you might not have realized there are two variants of the standard alert panel, because many Cocoa programmers don't use critical alerts when needed (instead they use the generic NSRunAlertPanel() function, which is admittedly 50% easier to remember). For instance, in TextEdit, when you change a rich-text document to plain-text, and are going to irretrievably lose all your formatting information, you get an informational sheet, not a modal sheet. Whups.

Update: Mea culpa! I just checked and, in 10.4 at least, TextEdit can undo past a RTF conversion event, and so they were correct to NOT make their sheet be critical. I apologize to the authors of that app for impugning their character in my search for an example.

Invoking either these two panels from your programs is as simple as cake: you just call 'em and check the return code to see which button was pressed. Remember to follow the HIGs when it comes to the "title" and "msg," because a lot of people get this wrong. Specifically, the "title" isn't really so much a title any more under Aqua, it's more of a line of text describing the issue succinctly. This is a change from pre-Aqua days that some Cocoa programmers haven't fully grokked, so you'll still see panels with the title "Alert" or with just a title and no text, which the HIG guys hate.



--

So, after that brief discussion of application-modal panels, we move to document-modal sheets, which you should strive to use whenever it makes sense. First off, let's check out the easy-to-functions for these:

NSBeginCriticalAlertSheet(NSString *title, NSString *defaultButton, NSString *alternateButton, NSString *otherButton, NSWindow *docWindow, id modalDelegate, SEL didEndSelector, SEL didDismissSelector, void *contextInfo, NSString *msg, ...)
NSBeginInformationalAlertSheet(NSString *title, NSString *defaultButton, NSString *alternateButton, NSString *otherButton, NSWindow *docWindow, id modalDelegate, SEL didEndSelector, SEL didDismissSelector, void *contextInfo, NSString *msg, ...);

Hey, wait a minute... these functions have more parameters! I thought you said sheets were easier to use? Well yes, they are, for the user. For you: more work. However, the payoff is happy users. And you can't put a price on that. Well, you can, actually: it's about $40, which is pretty sweet.

The big difference in running sheets vs. running panels is that these sheet functions return immediately, before the user has responded to the panel. Note that they don't return an int, they return void: the great unknown. (If only we had Schrödinger's return type, which would represent the all and none of default and alternate and other.)

Now, this immediate return throws a lot of programmers of, and rightly so... I mean, obviously you are throwing up an alert because you want some feedback from the user, and so the _very_ next thing you want to do is going to be based on what the user decides, and yet here you are, unceremoniously dumped back into your code with no decisions in sight. What to do?

Well, there are two choices. One is, just immediately return from whatever method you're in, which should return control flow to the main even loop, and then wait for your didEndSelector to eventually be called when the user makes up her mind. In that method, you'll be informed of her decision, and can proceed naturally.

The only downside to this is that if you've built up a lot of state information before popping up your alert sheet, you're going to have to regenerate it when you get called back or you'll have to save it off in your instance variables. Bluck!

One way around this, to be used in extreme cases, is to just process events on your own, such as the following:

Pulling Down an Alert Sheet, the Hard Way
@implementation ExampleClass

- (IBAction)doSomethingCompletelyAmazingYetKindOfDangerous:(NSButton *)sender;
{
[some code that has a lot of state setup]

userChoice = INT32_MAX;

NSBeginInformationalAlertSheet(NSLocalizedString(@"I'm Going to do Something Big", @"alert title"),
nil, nil, nil, [document windowForSheet], self, selector(_sheetDidEnd:returnCode:contextInfo:),
nil, NULL, NSLocalizedString(@"It's going to be awesome. Seriously.", @"alert message"));

while (userChoice == INT32_MAX) {
[NSApp sendEvent:[NSApp nextEventMatchingMask:NSAnyEventMask
untilDate:[NSDate distantFuture] NSModalPanelRunLoopMode dequeue:YES]];
}

if (userChoice == NSAlertDefaultReturn) {
[some code that does the cool thing with the state we calculated at great expense]
}
}

@end

@implementation ExampleClass (Private)

- (void)_sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
userChoice = returnCode;
}

@end

In this example we assume userChoice is an instance variable (quick quiz: why can't this be a static variable instead?) and is an int. I'm not particularly fond of having a one-use ivar like this, but, hey, not a lot of options - if the alternative is saving off four or five different pieces of state in ivars, this might be cleaner. Note that MOST of the time you're really going to want to do your actual processing in the -_sheetDidEnd:..., so you can skip all the NSEvent crap and just return from your -doSomething... method -- this is just an example of how you might work around a situation where you really don't want to return from your action method before you finish what you're doing.

Update: Pierre Lebeaupin (the pretty pin?) correctly pointed out that my contrived example above is, in fact, wrong in the case of document-modal sheets (see the comments page). I tried to be weaselly in this example by saying, "I'd only use this in extreme cases," to express that I've never used [[sendEvent:...]nextEvent:...] hack in this way, but it'd be something I might try (and then, hopefully, discovered the bug Pierre found).

What I was trying to accomplish was come up with a concise way to introduce sub-event loops, which are actually a useful tool in some extreme situations. In fact, this post was inspired by one of my programmers not knowing about them when trying to work around some problems with printing, so I tried to blend my discussion of sheets and event subloops into one mélange, but, in fact, my example was flawed. Pierre pointed out a fundamental problem that, in fact, early Mac OS X apps all suffered from, because early on Apple hadn't thought of the case where you pop up two sheets in two different documents. Apple added the asynchronous APIs just for this case.

Here's a real example of when you might use your own event subloop, which may in fact still have problems of its own (we just started doing this in our code), but I assert it does not due to the inherent limitations of the print system.

Right now, the way we print in Delicious Library 2 is to create a WebView (from Apple's WebKit) and populate it with your books &c, then just tell the WebView to print itself. Clever, no? The only problem is that WebViews will NOT load images synchronously (grrrrrr) even if they are local images and even though it would be completely trivial to do so (eg, NSURLConnection has a +sendSynchronousRequest:... method). So, if you're in the middle of running a print sheet for a document, and you create a WebView based on user input, and then want to print it, you need to spin on the event loop letting all the images load asynchronously before you try to print the WebView. Since there appears to only be one possible print operation per application, I believe this to be a valid use of a event subloop. In our case, the code looked like this:

A Valid Use of Event Subloops
- (void)_setupWebViewThumbnailBasedOnUserSettings;
{
allImagesHaveLoaded = NO;

while (!allImagesHaveLoaded) {
NSEvent *nextEvent = [NSApp nextEventMatchingMask:NSAnyEventMask
untilDate:[NSDate dateWithTimeIntervalSinceNow:0.05] inMode:NSModalPanelRunLoopMode dequeue:YES]
if (nextEvent)
[NSApp sendEvent:nextEvent];
}

// elsewhere, we have code that sets 'allImagesHaveLoaded' when... well, you know
}
Note that we are, in fact, polling in the above loop, which is normally a no-no, but we are forced to because we're not guaranteed an event will be sent after the last image loads in our WebView, we don't want the user to have to 'bump the mouse' to get the print panel to finish. (This kind of mistake is very common when writing event loops, and in fact you can see it in a lot of shipping apps -- for instance, some older apps force you to keep moving the mouse to keep autoscrolling; if you stop moving, you stop scrolling, because events aren't being processed and they didn't set a separate timer event like the Apple docs said.)

In this case, we don't mind the polling so much because we know we'll stop once we've loaded a very small number of images from a local disk, and because there's really not much else the user is planning to do in this quarter-of-a-second (switch applications and start and a compile? not likely.) Since the end of the polling is NOT determined by user input, we aren't in a situation where the user could wander away from her computer while it is polling while waiting for her and come back to find her MacBook slagged from overheating.

--

O'Reilly has written a whole article about running alert sheets, which you can read for a lot more detail than I would ever provide you with, ever. Finally, you should check out the (relatively) new NSAlert class, which is slightly more complex to use (you have to create the object and THEN show it) but provides an objective API onto all this.

--

Ok, now we've handled the case of pulling down a simple yes/no/maybe style sheet with two lines of text and an icon. What if you want to pull down a sheet with radio buttons and tables and all kinds of fun widgets, that performs a relatively complex task for the user (like, say, configuring rules for a smart shelf)? Here's an example sheet from the AppKit, of setting up a fax. (Note that if you want to do exactly what's on this sheet, you should, you know, just send a fax.)



Well, here's what I consider to be the current best practice for sheets that have a ton of logic and interaction. The overview is, you create an NSWindowController subclass to handle loading and unloading the NIB with the sheet in it (remember that a sheet is just a standard window to Interface Builder), and then provide a single, beautiful class method as its API:

More Complex Sheet Handling
@interface LIShelfCreationSheetWindowController : NSWindowController
{
NSDocument *document;
// [add some ivars and outlets and stuff]
}

// Class API
+ (void)runModalSheetlToCreateShelfForDocument:(NSDocument *)document;
// actions
- (IBAction)addShelf:(id)sender;
- (IBAction)cancel:(id)sender;

@end

[...new file...]

@implementation LIShelfCreationSheetWindowController

// Class API

+ (void)runModalSheetlToCreateShelfForDocument:(NSDocument *)document;
{
LIShelfCreationSheetWindowController *shelfCreationSheetWindowController =
[[self alloc] _initWithDocument:document];

[NSApp beginSheet:[shelfCreationSheetWindowController window]
modalForWindow:[document windowForSheet] modalDelegate:shelfCreationSheetWindowController
didEndSelector:@selector(_sheetDidEnd:returnCode:contextInfo:) contextInfo:nil];
[[shelfCreationSheetWindowController window] makeKeyAndOrderFront:nil]; // redundant but cleaner
}

// actions

- (IBAction)addShelf:(id)sender;
{
//
// actually create the shelf, based on the user's settings on the sheet, which are bound to our ivars
//

[NSApp endSheet:[self window] returnCode:0]; // stop the app's modality
}

- (IBAction)cancel:(id)sender;
{
[NSApp endSheet:[self window] returnCode:0]; // stop the app's modality
}

@end

@implementation LIShelfCreationSheetWindowController (Private)

// Init and dealloc

- (id)_initWithDocument:(NSDocument *)aDocument;
{
if (![self initWithWindowNibName:NSStringFromClass([self class])])
return nil;

document = [aDocument retain];
return self;
}

- (void)dealloc;
{
[document release];
[super dealloc];
}

// private callbacks

- (void)_sheetDidEnd:(NSWindow *)sheetWindow returnCode:(int)returnCode
contextInfo:(void *)contextInfo; // [NSApp beginSheet:...]
{
[sheetWindow orderOut:nil]; // hide sheet
[self autorelease];
}

@end

Things to note here: this class has a backpointer to your NSDocument, so it can get at any relevant state it needs without you having to passed it in explicitly as parameters to the class method (in real code you'd replace the word "NSDocument" with the name of your NSDocument subclass). For instance, we immediately need to figure out which window will sport our shiny new sheet, and we use NSDocument's built-in and conveniently titled -windowForSheet method.

In NIB you'd hook up buttons to invoke the -addShelf: and -cancel: actions, and you'd using bindings to keep your instance variables in sync with the tables and radio buttons and other widgets in the UI, so you can just access your ivars directly in -addShelf: when the rubber hits the code.

I prefer having a separate class to run the sheet because I've found there can be a lot of cruft code for configuring the UI and performing some action, and it's nice to offload your NSDocument subclass of that kind of code. In NSDocument, your original action method is going to be very small (one line!), which is going to make it very readable.

--

This shell of a class isn't intended to evoke a "Genius!" reaction from the gentle reader: hopefully, in fact, you are muttering, "That seems pretty obvious." Because the correct way to write code should always seem obvious, in retrospect. Years from now, when you're looking at your code, you should say, "Of course I wrote it that way -- there's no other decent way to do it," and not, "What the hell was I thinking?"

Labels:

Saturday, April 15

Pimp My Code, Part 10: Whining about Cocoa

While I'm almost always a cheerleader for Cocoa (and Apple in general) in public, there are, of course, boneheaded things in Cocoa that grind at me every time I run up against them. Some annoyances exist for historical reasons (eg, Why does NSImage have twenty different ways to draw itself, each of which has slightly different semantics?), and some are apparently boundary problems between engineering groups (eg, Why doesn't NSImage have a way to create itself from a CGImageRef? Why are NSRect and CGRect exactly the same yet different? Why can't I ask an NSCachedImageRep for its CGContextRef?).

And, of course, some things are just plain old slap-your-head-duh mistakes...

Bindings, for instance, are my second-favorite feature in all of Cocoa (after CoreData). They're great. I love 'em. So much so that when any little part of them is less-than-perfect, it's incredibly grating.

NSTreeController is a new class designed to drive an NSOutlineView using bindings, which parallels NSArrayController's ability to drive an NSTableView. (Not surprisingly, they are both subclasses of NSController.) However, NSTreeController's design has a number of problems, most of which stem from this very questionable design decision (from Apple docs, emphasis mine):

- (id)arrangedObjects; // opaque root node representation