Archive for the 'Best practices' Category

iPhone app settings

Wednesday, January 7th, 2009

One of the ongoing debates among users of iPhone OS devices is whether an app’s settings belong in the app itself, or the Settings app.

I’m of the opinion that this wouldn’t even be a debate if it weren’t for Apple’s prescription in the iPhone HIG that every iPhone app’s settings should be in the Settings app. Mac apps don’t come with prefpanes for their preferences (with the exception of faceless background apps like Growl). Windows apps don’t, either, that I know of. GNOME and KDE apps don’t pollute Ubuntu’s Control Panel.

The iPhone is the only OS I know of whose developer recommends that app developers put their application settings in the system-wide Settings app.

As we’ve seen several times on every platform, it’s OK to break one of the local Human Interface Guidelines if and only if the violation makes the interface better.

I think this guideline is one that iPhone developers should violate flagrantly.

But there’s a problem. The iPhone doesn’t really have an icon for a Settings button. Most developers seem to use the Info icon that one of the frameworks apparently provides, but this isn’t the proper use of that icon. The Info icon means info, not application settings.

Another choice is the gear icon for Action buttons:

NSActionTemplate.

But, again, we have a conflation of functions. The button in question is not an Action button; it is a Settings button. This icon is not a Settings icon. (I suspect that most people who use the Action icon use it because it doesn’t have any particular association with “action”, either, other than Apple’s endorsement of it for that.)

The Iconfactory, wisely, chose differently in Twitterrific. I suspect that this was largely coincidence, as the Mac version of Twitterrific came first and already had a Settings icon; for the iPhone version, the developers simply used the same icon. It works well enough:

as seen in this screenshot of Twitterrific.

But it’s not perfect. A wrench does not say “settings”. (I offer myself as evidence: When I first saw it in the Mac version, I didn’t know it was the Preferences button.) Generally, a wrench means “edit this”, as in the context of a game.

What we need is an icon that says “settings”. Ideally, this icon should either convey the notion of a changeable state value (as the previously-favored light switch [Mac OS X through Tiger] and slider [Mac OS] did), or build on an existing association with the concept of settings.

Let’s go with the latter. I nominate the Settings app’s icon:

iPhone Settings icon

Familiar enough, wouldn’t you say?

That’s Apple’s version. Here’s my button-icon (a.k.a. template) version, in the 16-px size:

Settings button icon 16-px version.

I tried it out in the iPhone version of Twitterrific on my iPod touch. Before and a mock-up of after:

Before.
After.

After I created this icon, I wondered what it would look like in the Mac version of Twitterrific.

Here’s the original:

…with the wrench icon.

… And right away we have a problem. These buttons are already framed; my white frame will glare here.

Fortunately, that’s easy to solve. With ten seconds of work, I created a frameless version. Here’s what that looks like:

Twitterrific-Mac-newSettingsIcon.png

I think we could all get used to this icon. This wouldn’t have worked at all before Apple changed the icon of System Preferences to match the iPhone Settings app, but now it can.

I don’t think it’s perfect. Perhaps a real icon designer (I’m just a programmer) can refine it. But I think it’s a good first draft. I’m curious to hear your opinions; please post constructive feedback in the comments.

If you want to use this icon, go ahead. Here’s the original Opacity document , from which you can build all the many variations of the icon. (Click on Inspector, then Factories, then find the version you want in the list and click its Build button.)

Updates to “How to work with a bound-to array”

Wednesday, December 3rd, 2008

Following an email conversation with Dave Dribin, I’ve updated my earlier post on how to work with a bound-to array.

The most critical update, and the reason for a new blog post to announce the update, is that you should not currently implement addObject: and removeObject: for an array property. KVO has a bug where it treats these as set accessors, even if the property is an array. This will result in crashes if you observe your object using plain KVO.

The workaround is to use indexed accessors:

[self insertObject:newFramistan inFramistansAtIndex:[self countOfFramistans]];

This is a little uglier than addFramistansObject:, but it doesn’t crash.

The other update is that you need to implement both accessors in a pair. For example, for insertObject:inKeyAtIndex: to work, you must also implement removeObjectFromKeyAtIndex: (and vice versa). Otherwise, KVO won’t override the one you did implement, so you won’t get notifications for it.

I’ve filed bugs for both of these, and I’ve linked to both bugs from the older post.

How to work with a bound-to array

Wednesday, November 26th, 2008

You have a model object with an array property:

@property NSArray *framistans;

You also have an array controller whose contentArray binding is bound to this property.

How do you add an object to the array?

Wrong #1: Direct manipulation

[framistans addObject:newFramistan];

The array controller will not notice this change. Worse, the value of your array no longer matches the value your array controller last saw. (For one thing, your array is now one object longer.)

Wrong #2: Use the array controller’s add: action

[arrayController add:nil];

This works, but now you need to go looking for that object. The proper way would be to get the binding info for the array controller’s contentArray binding, then ask the bound-to object for its array, and get the lastObject of that array.

More likely, you’ll hard-code knowledge of which object and which property it’s bound to. Good luck when you change the binding!

Oh, and in a model object, this breaks the separation between your model and everything else. Your model should know nothing of your UI, so that you can replace the UI wholesale if ever you want to (for example, if you make a CLI or web-based version of your app) and keep the same model.

Wrong #3: Add the object to the array controller’s content array

newFramistan = [arrayController newObject];
[arrayController addObject:newFramistan];

This works, and at least you already have the object on hand. But you’re still giving your model knowledge of the array controller, so you’re still breaking separation.

Almost right: Add it to the mutableArrayValue for your property

[[self mutableArrayValueForKey:@"framistans"] addObject:newFramistan];

This works, and it’s clean.

The only problem with it is that it’s too heavyweight: you’re creating this proxy object just to mutate a property. The most appropriate use for mutableArrayValueForKey: is if you want to pass a mutable array to another object, and you want that array to actually be a property of another object. This is a very rare case—I’ve never needed to do it.

For this problem, there is a better solution.

The Right Way: Use your accessor

UPDATE 2008-12-02: Don’t do this. See the section I added below.

[self addFramistansObject:newFramistan];

Look at that.

Beautiful, isn’t it?

It’s almost as short as the direct array access, and it does the Right Thing with KVO. Your array controller will find out about the change without you having to tie knowledge of the array controller into your model.

We should also look at the accessor:

- (void) addFramistansObject:(Framistan *)newFramistan {
    [framistans addObject:newFramistan];
}

Also short, and also beautiful. (Yes, that is the complete definition of the method. I’m not eliding anything.) And there are no separation violations here, either: It’s pure model.

When you bind the array controller to your object, KVO wraps your object and all its KVC-compliant accessors, including this method. Its implementation performs the proper KVO notifications around a call to your implementation, which means you don’t have to do any KVO work at all.

UPDATE 2008-11-30: Note that you must also implement removeKeyObject:; if you don’t, addKeyObject: will not post the KVO notifications. You must implement both. Thanks to Dave Dribin, who emailed me about this.

ADDED 2008-12-02: The current Right Way: Use indexed accessors

KVO does not work correctly with addKeyObject: and removeKeyObject: for array properties.

The problem is that it treats those methods as set accessors and posts set-mutation notifications, regardless of the fact that it’s an array property. This doesn’t seem to cause a problem with Bindings (as of 10.5.5), but when I tried observing the property using KVO directly, I got a crash every time, as it tried to send NSSet messages to my NSArray.

Instead, you’ll need to use indexed accessors:

[self insertObject:newFramistan inFramistansAtIndex:[self countOfFramistans]];

As with addObject: and removeObject:, you must implement both of the pair. Here’s what removal looks like:

[self removeObjectFromFramistansAtIndex:[self indexOfObjectInFramistans:framistanToRemove]];

indexOfObjectInKey isn’t actually something KVC looks for, but it fits the use case and beats getting the entire array (which generally means copying it) just to find the index of one object. And you have to implement all these methods anyway.

Note that you should not implement addKeyObject: and removeKeyObject: at all for an array property, even to call the indexed methods. That’s because KVO always posts its erroneous set-mutation notifications around your addKeyObject: and removeKeyObject: methods, regardless of how you implemented them.

Thanks again to Dave Dribin, as most of this came up in the same email thread.

Further reading

New service: Insert Mac OS X Build Number

Friday, November 21st, 2008

File: InsertMacOSXBuildNumber.zip

A service that inserts the build number (for example, 9F33) of your current Mac OS X installation.

The main purpose for this is so that, when filing bugs in Radar, you can precisely specify which build of Mac OS X you’re running. (Especially if you’re running a pre-release build of a future version of Mac OS X.)

I created it with ThisService, of course.

(And yes, I am also working on tonight’s Framework Friday post.)

Tabs vs. spaces redux

Wednesday, November 5th, 2008

Alice, who prefers four-space indents, wants this:

Four-space indents, with non-first lines of an Objective-C messages lined up on the colon of each argument.

Bob, who prefers eight-space indents, wants this:

Eight-space indents, with non-first lines of an Objective-C messages lined up on the colon of each argument.

Is it possible for this source code file to be written such that Alice and Bob each see what they want?

Yes, but not yet.

What they need to do is to use tabs only up to the level of indent of the first line, then spaces the rest of the way:

Screenshot demonstrating this with four-space indents.
Screenshot demonstrating this with eight-space indents.

Note that the contents of the file are the same in both of these images; the only change is the indent width. The characters shown in these two images produce the result shown in the earlier two images.

I say “not yet” because no editor currently does this. Xcode, for example, will use on each line as many tabs as it can fit, followed by < tabstop spaces. Other editors won’t line up the colons; they’ll just left-justify all of the non-first lines.

So Alice and Bob can each have the indent width they want; it’s just a pain in the ass for them to do the editing necessary to get it. Therefore, they won’t bother, just as I don’t.

So, consider this my call to the makers of all the editors to auto-indent the right way, as I have just shown it.

Added a few minutes later: Alternatively stated by Christopher Bowns on his blog post.

Things your ReadMe must include

Tuesday, May 20th, 2008
  • The name of your application
  • What it does
  • How to configure the application, if necessary (note: does not include installation)
  • Simple overview of how to use the software, once it’s configured (detailed manual should be in the Help menu)
  • How to uninstall the software, if necessary (i.e., if it isn’t an application)
  • FAQ
  • What it costs, if it’s not free
  • How to register it
  • A link to your website (in case a magazine distributes your app on its CD)
  • Contact information for support questions
  • Contact information for sales (registration/pricing/currency) questions

Things that you may want to include, but aren’t necessary

  • Screenshots
  • Troubleshooting information

Things that you shouldn’t include

  • Installation instructions: If it’s a plain app, you don’t need an installer; otherwise, make an Installer .pkg. In either case, you shouldn’t need instructions.

Formats I approve

  • RTF or RTFd
  • HTML or webarchive
  • Plain text

You may also want to provide a trampoline application to open a localized version of your ReadMe (for example, see the ReadMe on the Mac OS X DVD). Bonus points if you create a kit to make these, for the benefit of other developers.

Formats I disapprove

  • Word or OOXML format: Many people don’t have Word, and everything else handles these documents imperfectly. Use RTF instead.
  • OpenOffice format: Many (probably most) people don’t have OpenOffice. Use RTF instead.
  • PDF: Use PDF either for vector graphics (inside your app) or for documents you expect someone to print. If the user is going to have to print your ReadMe, you need an interface overhaul.

As usual, I invite suggestions, rebuttals, and amendments.

UPDATE 2008-05-24: Recommended including contact information, as suggested by ssp.

Multiple methods found

Friday, March 14th, 2008

I wrote some code like this:

[[QTMovie movieNamed:sound error:NULL] play];

and Xcode gave me three warnings:

  • GrowlApplicationController.m:537: warning: multiple methods named ‘-play’ found

    • /Developer/SDKs/MacOSX10.4u.sdk/System/Library/Frameworks/AppKit.framework/Headers/NSSound.h:47: warning: using ‘-(BOOL)play
    • /Developer/SDKs/MacOSX10.4u.sdk/System/Library/Frameworks/QTKit.framework/Headers/QTMovie.h:340: warning: also found ‘-(void)play

All warnings are bad, but these are even worse: The compiler is telling me that it’s picked the wrong method. I’m creating a QTMovie, so I want it to use -[QTMovie play] (the one that returns void), but it picked -[NSSound play] (the one that returns BOOL).

Now, in this case, it won’t matter, thanks to Obj-C’s dynamism—all it will do is send a play message to the object, and the object will use whatever play method it knows about. In other words, despite what the compiler is telling me, the program will actually call the correct method (the one in QTMovie).

But this could be a lot worse. If I’m expecting, say, a structure type, and the compiler picks a method that returns a scalar type such as int or float (or vice versa), the compiler will write the wrong assembly code for the message. My method will get gibberish back from the method it called, and Bad Things will follow.

The reason for this  problem is that +movieNamed:error: returns an id—not a QTMovie * specifically. For all the compiler knows, that method returns an NSSound *, or even an NSUserDefaultsController *.

Since the compiler doesn’t know what type the method returns, it must pick one from all the methods by that name that it knows about. You can’t rely on it picking any particular one, and I could only guess as to how it makes its choice. For all I know, it’s blind chance.

The ugly and naïve solution would be to tell the compiler what type we’re expecting by casting it:

[(QTMovie *)[QTMovie movieNamed:sound error:NULL] play];

Of course, besides being ugly, this specific code still has the problem of not accepting an error object. What I should do, and will do, is accept the error object and stash the returned movie in a variable, so that I can test it and then either report the error or play the movie.

NSError *error = nil;
QTMovie *movie = [QTMovie movieNamed:soundName error:&error];
if (movie)
    [movie play];
else
    [NSApp presentError:error];

This code is longer, but it will always use the correct method and it doesn’t ignore the error. (You may also have noticed that I change the previously-misleading name of the name variable from sound to soundName.)

How to set up your custom view to work with services

Friday, January 4th, 2008

If you’ve used any Cocoa-based text editors at all, especially TextEdit, then you know that NSTextView provides free support for text-based services. Indeed, this is what makes ThisService so great: there are dozens of programs that work with text at the command line, and those programs are just as useful in the Services menu.

But what if you’re not working with text? What if you’re in, say, an image editor, and you want to insert a screenshot from Grab? (I don’t mean to pick on Acorn; that’s just the situation that gave me the idea for this post.)

This is one of those rare things that you have to write code for. However, like most things in Cocoa, it takes very little code.

I’m going to give you the test app right up front. You should download that now so you can play along at home. It accepts any image, such as from the Grab services.


The first thing you need to do is tell AppKit that you can handle certain types. You’ll do this by implementing this method in one of your NSResponders—probably a custom view:

- (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType

This method will be called a lot, so try not to take too long in this method. Fortunately, your implementation will probably be very simple. Here’s the implementation from the test app:

Here's a diagram showing what “send” and “return” mean in the context of a service.
- (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType {
    if ((sendType == nil || [sendType length] == 0U) && [[NSImage imagePasteboardTypes] containsObject:returnType])
        return self;

    return [super validRequestorForSendType:sendType returnType:returnType];
}

The sendType is the pasteboard type of data that you will send the service (its input), or nil* if it doesn’t take input. The returnType is the pasteboard type of data that the service will return to you (its output), or nil* if it doesn’t give output. In Leopard, they can also be UTIs; you’ll be called with both the pasteboard type and the UTI.

If you can handle that combination of send and return types, then return the object that will handle it. That object, whether it’s self or another object, must conform to the NSServicesRequests protocol.

If you can’t handle that combination of types, call up to super, and return that.

Your return value to this method controls whether the service’s menu item will be enabled or disabled. For this reason, if your custom view allows a selection (such as a range of the view’s text, or a sub-shape of its image), you may want to consider the selection. In particular, you may or may not want to say you can send the service data if the user hasn’t selected anything.

Note: On Leopard, your view must be in the responder chain, or this method will not be called. Thus, you may need to add code for it to become the first responder. If this method doesn’t get sent to any object, or if it does but doesn’t return an object, you don’t get any services.

* In both cases, the documentation says empty, which would be @"", but reality disagrees.


Exchanging data

If you’ll be providing input to the service (such as some text to TextEdit’s “New Window Containing Selection”), then you need to implement this method:

- (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard types:(NSArray *)types

The name is not an accident. The idea is that, if the user can select a portion of the data, this method should consider that selection.

The types array is of pasteboard types (and probably UTIs, on Leopard—but I haven’t tested this part) that the service is ready to accept from you. Don’t panic if you can’t provide all the types; just provide all the types that you can provide efficiently. Then, return YES.

If you can’t provide any of the types in the array, return NO.


If you’ll be receiving output from the service (such as a screenshot from Grab, or a result from a service such as Script Editor’s “Get Result of AppleScript”), then you need to implement this method:

- (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard

Replace the currently-selected data with the new data from the pasteboard, then return YES. If there’s no data on it that you can use, return NO.

An invalid property list

Monday, October 1st, 2007

If you work on an [added: a document-based] app that uses a plist-based format, you should test your plist-reading code to see how it handles an invalid (e.g., eaten by the user’s dog) plist. Here’s one:

<plist>
<qux></qux>
</plist>

The binary plist parser won’t even touch it because it doesn’t have the bplist header, the XML parser will choke on it because it contains an element that it doesn’t recognize, and the OpenStep parser will choke on it because the first line doesn’t end with a semicolon or backslash.

When you feed this to your app, your app should present an error message. Anything else is a bug. (UPDATE 2007-10-02: OK, maybe not. There’s at least one circumstance where you can ignore it, as pointed out in the comments. I still think I’m right in most circumstances, just clearly not all.)

Mac app checklist

Sunday, March 18th, 2007

A list of things that you must do for every app. Most of these should be done up front; some can be put off a little bit; some can be put off a long time, but ought to be done at some point rather than put off into the indefinite future.

Also available in TaskPaper format, in case you’re a TaskPaper user.

  1. Version control is mandatory. I recommend Subversion (which is free and open-source). The advantage of version-control is time travel: You can easily go back to any previous version of your code, or even certain files. This lets you delete with confidence. Be sure to follow the trunk+branches+tags layout for your repository; you can create branches for experimentation and tags for released versions. Both these operations are cheap in Subversion. The easy way to install Subversion is to use Martin Ott’s Subversion Installer package.

  2. Resolution independence. In particular, shun raster images (except for your Finder icons). Use vector graphics, either created in an app like Lineform or written in PostScript or Cocoa.

  3. Contextual menus. Much like AS support (listed below), effective contextual-menu support is a great way to make your app more efficient for its power users. In IB, create a top-level menu by dragging it from the palette into your nib, then ctrl-dragging from the view to the top-level menu and connecting to the menu outlet. If you create your views programmatically, you’ll need to override -menu (either returning a programmatically-created menu, or adding support for a -setMenu: method that you provide).

  4. Localize everything. Localize every string you show to the user, even if it’s only in English for now. If your app achieves any popularity (and you should hope it does), people will volunteer to localize it into their preferred language. If you use NSLocalizedString on every hard-coded string as you write it, your localizers will be able to localize your entire app, without having to ask you for app-side support because it will already be there.

  5. Maintain a bug- and feature-tracking system. Ideally, something like a locally-hosted Trac (don’t open it to the users, or you will be fighting lots of duplicates), but even a simple OmniOutliner or Opal document will save you having to remember all those things you need to fix/add.

    Its benefits aren’t as immediate as version-control: Version-control pays off the moment you want to bring back something you deleted, whereas a bug-tracker pays off months down the road, when you’re trying to remember what that last feature you wanted for 1.0 was, or what that one guy reported with the bug that only happened when he clicked on one of the windows with his left hand. But it’s the same kind of time-saver.

  6. Sell your shares in __MyCompanyName__. Especially important for open-source projects, since other people will be reading your code. Do this now, so you don’t need to do it after you create the project.

  7. Burninate NewApplication. The template nibs (including those in a new project) use “NewApplication” throughout the menus, where your application name is supposed to go. Make sure you replace “NewApplication” with your actual application name. (Even better, use Daniel Jalkut’s app template instead of Apple’s.) Take it from me, it’s embarrassing to ship an app whose menus still use the name “NewApplication”. ☺

  8. Thoroughly test everything in the stock menus. Especially the application menu (edit: used to say ; thanks Jesper) and Help menu. Services should work in any text view or text field, and ideally should work in any view that has data that your users may want to run a Service over. The Preferences menu item should be hooked up or deleted. Same for “Application Name Help”.

  9. Edit the credits. (No pun intended.) These are stored in the “Credits” file in Contents/Resources. The default Credits file is an RTF file; TN2179 says that the About panel looks for Credits.{html,rtf,rtfd}, in that order.

  10. An application icon. If you can’t draw a good icon, hire someone to draw one for you. (If yours is an open-source project and it becomes popular, somebody may volunteer.)

  11. Document icons. (For doc-based apps only, obviously.) One document icon for each document type you can write out, with different classes of type (e.g. movie vs. audio vs. text) being depicted in significantly different ways (e.g. QuickTime Player’s icons having sprocket-holes for video files vs. a waveform for audio files).

  12. Pick a creator code and register it. This still matters, especially for associating a specific document file with your app. (Say you’re writing a multi-track video editing app—please—and you want to have movie files that you save be owned by your app rather than QTP. That’s what a creator code is for.) Your creator code goes in the Properties tab of Target Info in Xcode. Keep in mind that creator codes are limited to MacRoman characters.

  13. Make sure your bundle ID, version, short version, and Get Info string are correct.

    • Your bundle ID should be something unique to you, and should use your domain—not Apple’s, not anyone else’s, yours. If you don’t have one, make one up. Be sure to use the domain that you’ll eventually actually own; when you finally register that domain (even if you don’t do it for years, but you should do it as soon as you can, IMO), you won’t have to change all your bundle IDs.
    • Dave Dribin discusses what to put in CFBundleVersion.
    • The short version string is the version that appears in Finder’s List View; it should be a simple major.minor.maint version-number string (e.g., 1.0), and nothing else.
    • For Get Info strings, include a copyright statement and your version—for awhile, Apple said to leave out the version, but they’ve since reverted that, as users (well, at least former Mac OS users like me) expect to see a version number there. The version number you have here should be the same one you have in the short version string.
  14. Run it with no prefs. The goal here is to see how your app looks and works the very first time it is run. In particular, anything fed by NSUserDefaultsController needs to handle nil values correctly if you didn’t put some default values into it in main.

  15. Do at least one private beta or release-candidate. This is your final attempt to discover any bugs that you haven’t found yourself. Maybe it’s a byte-order bug, or something that only happens in certain network situations, or a crash that doesn’t happen on your machine because xyz. Regardless, you’ll find the bug only when somebody else runs your program, and hopefully you’ll do that in a private beta or release-candidate phase rather than in your actual public release. (You’re welcome to do a public beta as well, but don’t stall—get to release quickly.)

  16. Include a license and a ReadMe. For open-source projects, picking a license is easy: Use new-BSD, MIT, LGPL, or GPL. I don’t know what you would do for a closed-source project—I suggest consulting an intellectual-property lawyer. And definitely include a ReadMe. Your ReadMe should cover the following points:

    • What does the app do? (This is in case it was delivered on a CD or other compilation. In that case, they didn’t read the description on your webpage.)
    • How would I use the app in its most basic operation?
    • Who owns this app? (Copyright statement.)
    • Where can I get a newer version of this app? (Link to your website for the product.)

    Also, if you present your ReadMe as an RTF file, make sure you choose “Prevent Editing” from TextEdit’s Format menu, so that your ReadMe file is read-only. This prevents your users from accidentally deleting important swaths of the file.

  17. Maintain a detailed version history. Include the version number, release date, and list of changes. Version control will help you here: It’s tedious, but not hard, to build a complete list of changes from an svn log. The version history should be on a page of your website.

  18. AppleScript support. This isn’t something you need right away, but it’s a definite plus, especially if you’ll be selling your app. Daniel Jalkut has good info on adding AppleScript support. Even just being able to script the preferences is a good start.

  19. Check for leaks. The leaks(1) utility will scan your app’s heap looking for memory that isn’t pointed-to anymore. It’s very easy to leak objects, especially when your autorelease-fu is not strong. Be sure to use MallocStackLogging, so you can see what allocated the leaked memory; this may give you a hint as to where the leak lies. You may also find heap(1) useful.

  20. Get a real website. No, your GeoCities website won’t cut it. Expect lots of traffic, and be on a server that can handle it. No such server is free. (Also, don’t host it yourself—most ISPs won’t allow you to run a server on a consumer account, you don’t have the bandwidth anyway, and professional hosting is cheaper than an ISP server-grade account.) I recommend TextDrive, though others[1] [2] [3] have their own recommendations.

Feel free to suggest more items in the comments.

UPDATE 2007-04-02 15:15 PDT: The old #5 was controversial, and I concede that it’s too early to give that advice yet. I’ve replaced it with one that I posted in a comment. For posterity, here’s the old #5:

Test on both architectures. If you can’t do this, make your app Intel-only. Testers with ICBMs are easy to come by. Testers with PowerPC Macs aren’t so much, and they get harder to find every day. So if you have only a PowerPC, you can recruit somebody with an ICBM to test your UB, but if you have only an ICBM, it’s easiest to just make it Intel-only.

UPDATE 2007-04-02 20:17 PDT: Added a link to Martin Ott’s Subversion Installer package.

UPDATE 2007-07-23 20:15 PDT: Added discussion of the short version string to point #13.

UPDATE 2007-07-23 20:27 PDT: Formatted point #13 and clarified its discussion of Get Info strings.

UPDATE 2007-08-06 04:30 PDT: Added links to OmniOutliner and Opal.

UPDATE 2007-08-08 17:15 PDT: Added TaskPaper version.

UPDATE 2007-08-08 21:51 PDT: Added suggestion to Prevent Editing on your ReadMe.rtf file.

NULL as a function argument should be illegal

Sunday, February 25th, 2007

I’m going to confess a realization that I’ve come to regarding one of my oldest and strongest API-designing habits—that I now believe that habit to be a mistake.

Whenever I design an API, my mind habitually wanders to how its functions should handle NULL for various arguments. Usually I make NULL do something special. In this way, I give NULL the implication of “do this operation with what you already have”, or “do this with the default value for this argument”.

This is a mistake. NULL should be an error.

There are two reasons why this is so:

  1. Clarity

    Compare these two pieces of code*:

    remove_filter(NULL, NULL);

    remove_all_filters();

    Which is clearer? Which makes the statement’s action more explicit?

  2. Robustness

    What happens when (not if) somebody passes NULL by accident?

    Consider this:

    hookName = [filterNameField stringValue];
    [self removeFilterForHook:hookName
    target:nil
    selector:NULL];

    What happens when you forget to connect the filterNameField outlet in Interface Builder, or when one of your localizers accidentally disconnects it, or when you or another programmer on the project deletes filterNameField, replaces it with something else, or cuts-and-pastes it (e.g. into a new tab view item), and fails to reconnect it?

    When the outlet is not connected, filterNameField will at runtime be nil. Messages to nil return nil, so hookName is assigned nil. Then you pass this value (nil) as the hook name to removeFilterForHook:target:selector:**. Oops—you just removed all the filters.

    The solution is to add a new method called removeAllFiltersForHook:, and make nil/NULL illegal in all four arguments (the one to removeAllFiltersForHook: and the three to removeFilterForHook:target:selector:). Thus:

    hookName = [filterNameField stringValue]; //stringValue returns nil, as before
    [self removeAllFiltersForHook:hookName]; //Assertion failure

    Rather than removing those filters and causing invalid (unfiltered) data to pass through, which will hopefully be noticed now but Murphy’s Law says will be noticed much later (after you have forgotten this code), the new code comes to a screeching halt with a message in the console. If your program is seriously robust, your crash-catcher (for you do have one) will catch the exception and present a crash-report dialog.

    And, of course, your code is much clearer—no longer must the maintenance programmer (you, in six months) think about and interpret what you meant by “target:nil selector:NULL”, as such magic has been banished.

Counterpoint, and the rebuttal to the counterpoint

The main reason why I’ve made functions accept NULL and handle it specially is so that I can implement some higher-level method (such as removeAllFiltersForHook:) with it. For example, a function in Growl’s Carbon API:

Growl_RegisterWithDictionary

Register your application with Growl without setting a delegate.

Boolean Growl_RegisterWithDictionary(CFDictionaryRef dict);

When you call this function with a dictionary, GrowlApplicationBridge registers your application using that dictionary. If you pass NULL, GrowlApplicationBridge will ask the delegate (if there is one) for a dictionary, and if that doesn’t work, it will look in your application’s bundle for an auto-discoverable plist.

<rant>

Now, I ask you: What purpose is there for this magic NULL behavior? Why would you set a delegate (which the framework asks for a registration dictionary immediately), then explicitly register with no dictionary? What would you expect that to do?

This flagrantly violates the principle of UI design of “if you had to document it, it wasn’t designed intuitively”. The same principle ought to apply to API design. There’s already a Growl_Reregister function which you can use if your delegate’s registration dictionary changes, or if you didn’t have one and you just created it. There’s no reason why you should be able to call Growl_RegisterWithDictionary with no dictionary (for that’s exactly what passing NULL is) and have it do the same thing. What it should do is fail an assertion or something.

(For the record, I’m not calling out anybody else on the Growl Project on this. I designed that API, and I wrote the entirety of what I quoted above. All blame for this rests squarely with me.)

</rant>

The reason why I had this function act this way was so that Growl_Reregister could be a one-liner. Here’s the code (Growl BSD license):

void Growl_Reregister(void) {
Growl_RegisterWithDictionary(NULL);
}

Convenient. In turn, Growl_RegisterWithDictionary does some set-up, then passes the registration dictionary (whether it was passed in, obtained from delegate, or otherwise produced or assembled) off to another, internal function. I wouldn’t want to duplicate the code of Growl_RegisterWithDictionary, right?

But I don’t have to. All the set-up code from Growl_RegisterWithDictionary does is:

  1. If a dictionary was passed in, make sure that that dictionary is completely filled-in (and if it isn’t, fill it in from the delegate’s dictionary). This is handled by another function—Growl_RegisterWithDictionary just calls that function.
  2. If a dictionary was not passed in, get a filled-in dictionary from one or more other places.

If we make Growl_RegisterWithDictionary assert that its dictionary is not NULL, the code changes to:

  1. Assert that a dictionary was passed in.
  2. Make sure that the dictionary is completely filled-in.

Neither of these would be duplicated in Growl_Reregister—it takes no arguments, and the dictionary that it gets from elsewhere either is already completely filled-in by the time Growl_Reregister receives it or is not usable anyway. More to the point, any code that is using Growl_RegisterWithDictionary(NULL) would have to change to Growl_Reregister(), making it both clearer and shorter.

Conclusion

NULL should not be magic. It should not be special. For the sake of clarity and robustness, It should be illegal.


* The functions are named this way because I was thinking about WordPress at the time, but this really has nothing to do with WP, and I don’t know if it is even relevant at all. That is to say, I don’t know whether WP’s remove_filter treats null specially in any way. Case in point.

** Alert readers will note the similarity to NSNotificationCenter’s API. This is not intentional, but the similarity is real, and you should take from it that this situation is not merely theoretical: it certainly has happened, and will happen again.