Archive for the 'Best practices' Category

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.

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.