Dan Wood: The Eponymous Weblog

Dan Wood is co-owner of Karelia Software, creating programs for the Macintosh computer. He is the father of two kids, lives in the Bay Area of California USA, and prefers bicycles to cars. This site is his weblog, which mostly covers geeky topics like Macs and Mac Programming.

Useful Tidbits and Egotistical Musings from Dan Wood

Categories: Mac OS X · Cocoa Programming · General · All Categories

Thu, 01 May 2008

In Sandvox and iMedia Browser, we use a number of utility windows that are singletons; there will be only one of any of them open. Examples include the registration window, email list signup dialog, problem reporter window, and the release notes window.

I had noticed a lot of redundant code in each of these controllers' classes dealing with maintaining the single instance of that window, which we would store in a static variable and access with a lazy accessor, not unlike the technique here.

When something starts to get repetitive, it's time to find a better way. So I created a class KSSingletonWindowController which manages the singleton instances. Instead of storing each instance of a window controller in its own static variable, it maintains a static reference to an NSMutableDictionary holding the window controllers, keyed by an identifier (generally the subclass name).

The class contains four class methods:

@interface KSSingletonWindowController :  NSWindowController

+ (id)sharedController;
+ (id)sharedControllerWithoutLoading;
+ (id)sharedControllerNamed:(NSString *)registrationName;
+ (id)sharedControllerWithoutLoadingNamed:(NSString *)registrationName;

@end

To access a window controller, you only need to invoke sharedController on your subclass of KSSingletonWindowController. The object will be created if it is not yet registered, and then that single object will be returned to you. If you want to get the instance but only if it has already been loaded (useful, for instance, in clean-up code or a response to a method to close a window; there's no point creating it if it hasn't already been created) you can use sharedControllerWithoutLoading.

If you need to have more than instance of a class, you would need to register each one with a different name. For example, you might have one instance of RTFDWindowController which you use to show release notes, and another one for showing credits. You would use the methods sharedControllerNamed: and sharedControllerWithoutLoadingNamed:, passing in arbitrary strings to identify each unique instance.

Here is the implementation. Each method builds upon the previous ones.

static NSMutableDictionary *sControllerRegistry = nil;

@implementation KSSingletonWindowController
sharedControllerWithoutLoadingNamed: lazily instantiates the controller registry and looks up the entry for the given key.
+ (id)sharedControllerWithoutLoadingNamed:(NSString *)registrationName
{
  if (!sControllerRegistry)
  {
    sControllerRegistry = [[NSMutableDictionary alloc] init];
  }
  KSSingletonWindowController *result
    = [sControllerRegistry objectForKey:registrationName];
  return result;
}
sharedControllerNamed: looks up the named item. It creates and stores the object if it wasn't allocated yet.
+ (id)sharedControllerNamed:(NSString *)registrationName
{
  KSSingletonWindowController *result
    = [self sharedControllerWithoutLoadingNamed:registrationName];
  if (!result)
  {
    result = [[[self alloc] init] autorelease];
    [sControllerRegistry setObject:result forKey:registrationName];
  }
  return result;
}
sharedController and sharedControllerWithoutLoading call their "named" counterparts using the name of the class as the key. (Obviously, you would invoke +[YourClass sharedController], not +[KSSingletonWindowController sharedController] for this to work.)
+ (id)sharedController;
{
  NSString *className = NSStringFromClass(self);
  return [self sharedControllerNamed:className];
}

+ (id)sharedControllerWithoutLoading;
{
  NSString *className = NSStringFromClass(self);
  return [self sharedControllerWithoutLoadingNamed:className];
}

That should do it! Please feel free to use and adapt this for your projects. Enjoy!

Following the example of Panic and others, we've put some code into our applicatons that, upon first launch, will ask people if they want to join our email announcement list.

One thing I don't like is when computers are dumb. And what I thought would seem dumb would be the case where you are already on our email list, and you hear about our new app, so you download it from the link we provide, and upon launching said app, it asks you if you want to join the email list. Well, duh!

So I mulled this over in the back of my head for a long time. Should we have a special version of our application that skips this question? Wow that would be a lot of hassle and be potentially confusing. Or would there be some way we could use AppleScript to set the user defaults for our application so that it wouldn't ask? That would probably be pretty invasive and require the user running a script in ScriptEditor. Or download a helper application that sets the stage in advance, then launches the real application? Geez, that's not going to be obvious. There must be a better way....

Somewhere along the way, it occurred to me: Cookies. Yeah, that's it. We can access the cookie store from Cocoa; it's the same cookie store used by Safari or other Webkit-based browsers. True, some Mac users are using Firefox or Camino, but this might solve it for a good chunk of users.

So what I did was to store a cookie associated with our website onn the page that a person goes to when they have confirmed that they want to sign up for our mailing list. I also set that same cookie when somebody clicks on the download link in our email blast; the link takes them to a page that sets the cookie and starts downloading the desired file.

Our application, upon launching, first checks the user default that will be set to prevent being asked more than once. If that has not been set yet, it checks the cookie store. If the cookie indicating that the they are on our email list is present, then they will be spared the redundant question. Only if the default and the cookie are absent will they be bothered.

The cool thing is that this cookie works across multiple applications. The cookie check is built into iMedia Browser 1.1, which we just released this morning, but we will be building it into Sandvox as well. Once that cookie is set, they won't be asked again from any of our applications. Well, at least until the cookie expires — but by then they will have stored the user default to prevent being asked.

Clearly, this is not a fool-proof technique. The cookie store might not have been written to disk by the time somebody launches the application. The user might have cleared out their cookies, or isn't using a Webkit-based browser. But it's an interesting, light-weight mechanism for "communicating" between Browser and Desktop.

Naturally the next thing to think about is "What else could you accomplish with this technique?" You could set cookies on your company's website that your desktop application might be able to take advantage of, e.g. your most recent login ID (but not password of course), recently visited pages, and so forth. The "communication" could go the other way as well; a desktop application might set some cookies that help give the corresponding website some hints for web content.

This is a very "weak" form of communication. If you want to literally control your Desktop application from a website, you could put in hyperlinks to custom URLs that your application can respond to by implementing this kind of code. (For instance, this link, if you already have been running iMedia Browser, will open up the application's feedback window with some pre-populated fields; we've set this up to help people follow up on tech support issues.) And of course from your desktop application you can open up arbitrary URLs in your browser using NSWorkspace operations.

But when you want non-obtrusive passing of interesting data or settings between desktop and browser, this may be a useful technique.

Here is a snippet of code you can put into any subclass of NSValueTransformer that will cause it to automatically register itself when the class is loaded. This is useful for almost all value transformers, except perhaps those that you need to be given a parameter in the initialization process, such as these cool transformer classes.

This code will just cause the transformer to be registered with the name of the class itself. So if you class was, for instance, CondenseTransformer then you would specify "CondenseTransformer" in your nib file.

+ (void)load
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSValueTransformer *theTransformer = [[[self alloc] init] autorelease];
  [NSValueTransformer setValueTransformer:theTransformer
         forName:NSStringFromClass([self class])];
  [pool release];
}
Mon, 21 Apr 2008

Developers need to get Spin Dumps as well as Crash Dumps

Users of Leopard may have noticed that if you have to force-quit an application, you are prompted to submit a report to Apple, not unlike the crash reporting mechanism we are all used to.

As a developer, it occurred to me that this information would be very useful for me (and third-party developers in general) as well.

I did some investigating, and found that these reports are stored in a subfolder of Library/Logs/HangReporter/. (, it turns out, did some analysis of this folder a while back. Incidentally, it looks like the problem of the UNKNOWN folders has been fixed.)

Unfortunately, this directory, and the subdirectories for each application's reports, are root-only. I'm supposing that Apple put in this protection so that applications couldn't tell what other applications were doing, but the problem is that there is no way for the third-party application to reasonably look to determine if there is a new spin report that needs submitting. I don't think a user is going to tolerate being asked for an admin password every time they launch an application, just so the application can check to see if there are any new reports!

It would be much more useful if application-specific spin reports could be put into a user's home directory, and made readable to that user. Then, third-party developers could cobble together a mechanism for reporting a hang, just like many of us do for crash reports.

Saving spin reports is a really cool new feature in Leopard, but if there is no way to help the third-party developer, it's just a potential feature.

Filed as rdar://5879393.

Mon, 10 Mar 2008

If you are programming to target Leopard, a lot of common graphics can be found using -[NSImage imageNamed:]. Not every generic graphic can be found there; there are a lot of images in /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/.

Example quicklook of coretypes

It's certainly worth exploring the contents of this folder and possibly making use of the images. But since these images are not documented as an API, you will need to be sensitive to whether or not these images are still part of upcoming — or legacy — versions of Mac OS.

Since Sandvox is targetting either Tiger or Leopard, we have to be careful not to make use of a graphic that is in one big cat and not the other.

For our convenience, and for yours, I created a "diff" showing the contents of the Tiger vs. Leopard contents of the graphic files in that folder, after the break.

See more ...

Wed, 27 Feb 2008

We at Karelia have been recently going through a bunch of our source code, pulling out the general-purpose stuff apart from the Sandvox-specific stuff. Some of this is worth sharing.

Fellow Karelian Mike Abdullah has written a nice class that extends Core Data in a nice way. It's called KSExtensibleManagedObject. Check it out.

Tue, 19 Feb 2008

When we were exhibiting at Macworld Expo last month, we got the idea to make a screencast to show how we used Sandvox to create our Karelia Software website because several people said they hadn't realized our own site was built with our product. Hey, we "eat our own dogfood" but I guess it's not obvious!

We didn't act on it right away — the thought of doing a big production like we did for our Sandvox Introductory Screencast (We hired Clickable Bliss to make that for us) was a bit daunting. I was really hoping for a tool that would make it easy to create some informal screencasts from time to time.

Last week, Vara Software, an indie company based here in the Bay Area, introduced ScreenFlow. I immediately gave it a try, and I was quite impressed. It's a Leopard-only application (so plenty of good visuals and animation effects) and it is very nice. I dove into it for a couple of half-days, and I managed to create a screencast pretty quickly. It's not as polished as our main screencast, but I was able to get it looking pretty good!

After having a few false starts in which I struggled trying to narrate while going through my script, I read one piece of advice about how to make a screencast. Technology aside, the advice was: write the script, read it, and then "shoot" the screencast while listening to the voiceover track. I was much more relaxed after splitting the job into the "audio" and "video" portions.

The contents of this screencast are more about "showing" than "doing" since we have an already-built website. I was able to work in a couple of edits to the site within the screencast, most notably the insertion of a new entry in Karelia's Weblog highlighting the new screencast. A bit self-referential, if you ask me!

Post-production was where I needed to make the most intense use of ScreenFlow, to add in the title cards, remove flubs in what I said or what I did, and try to synchronize the words with the action. I could definitely feel that ScreenFlow was a "young" application (Vara just released 1.0.1 while I was in the process of editing) but I'm sure that most of the shortcomings I found (especially in manipulating the tracks) will be addressed soon.

I was very impressed with the ability to do transitions. The approach is quite unlike, say, iMovie, but it's pretty intuitive. I was able to whip up some nice graphics using Acorn and import them into my screencast at the beginning and end. There were a number of cool features that ScreenFlow provides that I didn't even take advantage of; perhaps I'll try them next time around.

I think the resulting screencast came out really well, especially considering how quickly I was able to put it together. I'm looking forward to doing more screencasts in the future with ScreenFlow. Anybody have any suggestions for what Sandvox topics to cover?

Sat, 16 Feb 2008

We have about 200 help pages for Sandvox, available at docs.karelia.com; also available from our Help menu. We recently added a feature, inspired by some Apple pages, in which there is a link to provide feedback at the bottom of each page. (See this page as an example.)

The link goes to a web-based form that allows the visitor to enter their opinion of the referring page. It's powered on the back-end by a simple PHP script that converts the form submission into an email message to us.

So far we have gotten a handful of good comments, but there are a couple of adjustments we had to implement to make this viable:

  • Prompt people gently that they will need to enter their email address if they are expecting a reply. We've gotten several submissions from people who use the form as a kind of tech support, hoping that we'll answer their question. It's hard to reply if they don't include an email address!
  • Spam-avoidance. We've had pretty good luck with honeypots. (I'd rather avoid a CAPTCHA if possible.) Essentially, we provide a couple of extra inputs that the spam-bots will probably fill in with spammish content. These inputs are not visible to the site visitor, so any submission with these fields filled in is very likely to be spam. Without this deterrant in place, most of our submissions have not been exactly relevant to the Sandvox....

I'm glad we added this feature to our documentation. Someday, I might change the link at the bottom of the page to do some "Ajax" magic and reveal a comment form right there on the same page, so that the visitor can see the page that they are commenting on, rather than taking them to a separate form. But that's #87,454 on my list of things to do.

Wed, 13 Feb 2008

Yesterday, I was interviewed by a couple of folks who are doing a project about the "indie" Mac software community. Many other people I know have also talked to them. I look forward to the report that they are working on.

It was interesting to reflect about the business, and the community that we have. This community is great — we "see" each other on Twitter (where I've been active for a while), iChat, email, and even in person at conferences like WWDC and Macworld Expo. It's something that certainly didn't exist when I "went indie" and started writing Watson and its unpublished predecessor, Museo, back in 2001.

There are dozens of well-known, successful independent Mac developers now. More seem to be taking the plunge each and every day. It's easier to do now because of the community, I think. Of course it also means more competition, and it's harder to get a quality application noticed amid the quickie applications. VersionTracker and MacUpdate are so full now that they are not particularly useful anymore.

There's one thought I expressed yesterday, which I'll reiterate here. Consider it advice for budding Indie software developers. If I could go back in time and give myself some advice, I'd say to plan for having more than one application, and incorporate that into your development and company infrastructure. Even if you are a one-product company for quite a while (as we have been, though I hope that will change soon), it will be much less painful to grow into a multi-application company. I'd even go so far as to work a "dummy" second application into your workflow, just so that you can easily work with a second real application should you ever have one. The "behind the scenes" aspects of the company, such as the structure of your source code repository, your building scripts and utilities, your payment processing methodology, your customer/mailing list databases, your techniques for looking up lost license keys, your online help documents, and so forth, will scale better if you plan for it early on.

Wed, 15 Aug 2007

Justin "Carpe Aqua" Williams reveals, on his Pinup CalendarBlog , his technique for organizing source code. I thought I'd link to it, since it's useful and very close to what we do. I don't have a strict template I follow (perhaps I should) but his approach is pretty close.

Some additions I'd suggest would be accessors (getters and setters; his "accessors & mutators" could be split up maybe), public methods (general functionality documented in the header file), and private methods or support methods, special functions not exposed to the outside world.

If you have lots of different kinds of delegate methods, you can always split up any of your categories into sub-categories (e.g. TextField delegate, Window Delegate, data source, etc. For a subcategory, you could forgo #pragma mark - to make the subcategory stand out less in the popup.

If you have a class that is really huge, it's actually helpful to split it up into multiple files. One ".h" file, declaring multiple categories, with the implementation of each category defined in separate files. For example, our document window controller subclass is so big, it merits nine files!

  • KTDocWindowController.m (general methods)
  • KTDocWindowController+Accessors.m
  • KTDocWindowController+ModalOperation.m
  • KTDocWindowController+Pasteboard.m
  • KTDocWindowController+SiteOutline.m
  • KTDocWindowController+SplitView.m
  • KTDocWindowController+Toolbar.m
  • KTDocWindowController+WebView.m
  • KTDocWindowController+WebEditing.m

A final hint: define a macro "mark" to insert the two pragma-mark lines, using Completion Dictionary.

Terrence has written a couple of really cool posts about work he's done that greatly speed up & simplify build processes in Cocoa & XCode....

I've been hearing complaints about how big PNGs are when saved by Sandvox, but I finally got around to testing out this issue. Check out these results.

SourceDimensionsFile Size
JPEG original before reducing & sharpening1200 x 1600912.1 KB
-[NSBitmapImageRep representationUsingType: NSJPEGFileType]
(quality 0.7)
480x640101.6 KB
-[NSBitmapImageRep representationUsingType: NSPNGFileType]480x640376.1 KB
Above PNG then processed by PNGCrusher480x640303.7 KB

Notice that the PNG file when saved from Cocoa are almost four times the size of the JPEG. Of course, the quality (0.7) is going to impact the JPEG size, but that's still quite a hit!

When I processed the output file through PNGCrusher, It was able to squeeze a good chunk of wasted space out of the file. This means that the compression algorithm supplied by Cocoa isn't very optimal. (It would be nice to have a parameter to pass in to determine how much to optimize, the tradeoff being speed of course.)

I wanted to get a sense of how Preview.app would do with this. I saved the scaled-down image from my program as lossless TIFF, then opened and saved as PNG from preview. The results were almost the same, just a few bytes in difference. It's good to know that at least Cocoa's API isn't worse than what Preview uses. As with the API, it might be nice to have some control over how optimized the compression should be!

I think I'm going to file two bugs to Apple. One requesting API access to better PNG optimization; another requesting Preview to control this optimization.