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:

Sunday, December 17

Marketing Irony.

Those of you in the Mac community may have noticed a bit of a kerfuffle happening over the last week concerning MacHeist and whether it is good for small developers. I won't recreate a laundry list of links for you here, if you've missed out just follow this google search. As well, I've already stated my opinions, on Ars Technica, so I'm not going to go over them again here in my little vanity blog.

However, I did want to mention one ironic point that gives me a chuckle. Several of the independent developers who are complaining how unfair MacHeist is treating the software companies that have participated in their bundle seem to be, well, almost disgusted by marketing. Marketing is this nasty thing that lessens the value of whatever it is you're trying to sell.

I, obviously, don't mind marketing, and in fact some might call me (without malice) a marketing whore. This is a difference of opinion, though, and I am not going to try to evangelize my position on this. If you think marketing is dirty, well, ok. Nobody is getting hurt with your opinion, except possibly you. It's not like you're saying, "I think we can't tax oil because grandmothers would freeze in New England," which statement I would feel compelled to refute.

So, the irony here is that EVERYONE is making out like bandits because of this controversy. Every day this week another blog or journal or tech site has written about the controversy, and it makes headlines on the various meta-sites, and more people click through the stories to MacHeist and the individual sites of the various software developers in question, and all of our sales increase. The guys who participated in MacHeist, the guys who objected to it -- we're all getting paid now. And we're getting paid to keep arguing with each other. Seriously, if I were a crasser man (yes, there is a limit, thank you) I would be writing other developers right now, saying, "Ok, now you call me a stupid-face, and then I'll call your mom fat, and then..."

When you're a small developer, eyeballs == sales, because your biggest problem is simply that people haven't heard of you. It really is the case that any publicity is good publicity. I don't care if you came to my site because you heard a rumor that I like to keep a goat and a chicken in my bathroom, you're still going to end up reading about my product, and about one in ten people who read about it try it out. Ka-ching.

Extremely cynical viewers might even assume this entire scandal was cooked up by the indie community as an attention-getting measure. Personally, I doubt the original objectors were that scheming, but I do get a chuckle out of the idea that they have, accidentally and unwittingly, used the basest form of marketing (scandal and controversy) and increased their sales, in their effort to decry the evils of marketing.

--

Imagine you spotted someone who was yelling in the middle of the street for some damn reason, and you approached them and asked them to keep their voice down in public, and someone else walked by and said, "Good point! Here's $1,000!" With that boost, that vote of confidence, wouldn't you, maybe, start looking for other arguments you could make in public? And if you got paid $1,000 again, don't you think you'd maybe take that positive feedback and eventually spend all your time arguing in public, maybe even standing in the middle of the street yelling?

I wonder if they'll give away this dirty money, or let themselves be infected by it. I'm going to keep it, myself -- I didn't start this argument, I just signed up to be in a bundle I thought would be a lark. But, hey, thanks for the extra customers, guys. If you'd like to debate, say, politics next time, I'll be right here.

-W

PS to other developers: Your mom really is fat. Seriously. She's so fat she has a trash bag for a sock. When she hauls ass it takes two trips. She's so ugly you could press her face in dough and make monster cookies. She's so hairy it looks like she has Don King in a headlock.

Labels: ,

Friday, December 1

Quel Horreur!

Today I got called out by Coding Horror for my Windows-hatin' ways. And it's true -- I do hate me some Windows like McAdams loves Gosling -- so regular readers would be forgiven if they expect me to work myself into a fine lather and then rinse Mr. Atwood with bile. (I wish I could work "repeat" into this strained metaphor, but it's early for me.)

Instead, I think I'll apologize. Yes, truly. I don't mean to demonize Windows users, or seem like an elitist. There are lots of people who use Windows who believe in the same things I do: clean code, beautiful and simple programs, and designing fun into every action. I'm even an occasional reader of Coding Horror, even though it's a... Windows blog.

I have a friend who is quite a bit richer and smarter than me (and who many of you have seen on YouTube); he works for Microsoft doing advanced research. He argues as passionately as I do for the same kinds of changes in our industry. He just thinks they are more likely to happen if he engages Bill Gates than if he engages Steve Jobs. Well, ok, I disagree, but I certainly acknowledge there's an opposing argument to be made.

Sure, I'm a zealot, but I'm not such a zealot that I don't know who's on my side. If you want to accomplish the same goals as me, and we merely disagree on the best method, you aren't "the enemy." I'm still going to argue with you loudly and vehemently, but, you know, in a nicer way.

--

I yell about the Mac and state things strongly, with plenty of hyperbole, because I am trying to start a revolution. Let me be clear: I believe in what I'm saying, and I don't stake out bizarre positions just to create controversy. I say things strongly to make the point that I believe in them strongly. It's very hard to convince anyone of your passion by whispering, unless you're the Godfather.

I know it looks like I have the world's biggest ego, with the Lotus and the app and the bragging and the FLAVEN, but that's not the intent. I simply want to say, "Here is what I have achieved. Here are all my cards. This level of success is available to you, and I will help you every step of the way if you embrace this revolution." I'm like those guys on late-night TV with the big smiles and the real estate and the "Would you like to make $10,000, $15,000, even $20,000 a MONTH from your OWN HOME with NO MONEY DOWN and NO EFFORT except SITTING ON YOUR COUCH and EATING CHEEZ-ITS and occasionally ROLLING YOURSELF OVER to prevent BEDSORES?"

There are still people who contact me and say, "I'm kind of thinking about maybe writing a program for the Mac... is it possible to make a living that way?" Yes, yes, yes, a thousand times, yes. I'm going to keep yelling it until everyone hears me. But please don't take offense if you don't want to join in the fun.

Labels: