Dan Wood: The Eponymous Weblog (Archives)

Dan Wood 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 older weblog, which mostly covers geeky topics like Macs and Mac Programming. Go visit the current blog here.

Useful Tidbits and Egotistical Musings from Dan Wood

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

Thu, 01 May 2008

An easy way to manage singleton window controllers

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!