wbyoung

Whitney Young is a developer at FadingRed
 

October 20th, 2009

Objective-C uses the convention of designated initializers to handle an instance’s initialization. The conventions set forth work very well in simple situations, but it seems that there are a few places in the Cocoa framework where the convention begins to break down.

There may be others, but two main problem spots are NSCell and NSCoding. The following discussion assumes that you’re trying to do simple initialization of an instance variable. For instance, allocating an array in the initialization of your object:

@interface MyObject : NSObject {
    id myArray;
}
@end

@implementation MyObject

- (id)init {
    if ((self = [super init])) {
        myArray = [[NSArray alloc] init];
    }
    return self;
}
- (void)dealloc {
    [myArray release];
    [super dealloc];
}
@end

How do you handle this in an NSCell subclass? The documentation for subclassing NSCell states:

The initImageCell: method is the designated initializer for NSCells that display images. The initTextCell: method is the designated initializer for NSCells that display text. Override one or both of these methods if you implement a subclass of NSCell that performs its own initialization. If you need to use target and action behavior, you may prefer to subclass NSActionCell or one of its subclasses, which provide the default implementation of this behavior.

So what do you do for initialization of a cell that displays both an image and text? Do you override one or both of these methods to handle initialization? Another problem with NSCell is that init does not call either initTextCell: or initImageCell:, so NSCell really doesn’t have a designated initializer.

You could make a new designated initializer initImage:(NSImage *)image textCell:(NSString *)text and make sure that all of the standard methods call this.

If you want to make a generic image text cell this way, you would implement the following:

  • init
  • initImageCell:
  • initTextCell:
  • initImage:(NSImage *)image textCell:(NSString *)text

All of the inherited initialization methods would call your designated initializer, and the designated initializer could just call the superclass init (then set the image and text). Now someone can create a cell with image and text and you’ve covered every way they could initialize it.

This seems pretty far from the simplicity explained in designated initializers. You should only have to override the one designated initializer from the superclass if you’re providing a new designated initializer.

If you’re creating a lot of cells, this can get pretty frustrating. There’s a lot of work to do just to get basic initialization working. It’s not over yet, though. Let’s now add the NSCoding protocol into all of this.

To support the coding protocol, you have to implement initWithCoder:. Designated initializers aren’t called from initWithCoder:. This makes initialization of a custom object can be created via decoding require the following:

  • init
  • initWithCoder:

Things that get loaded from plist files or from nib files fall into this decoding category. Guess what else that includes? Cells! So now a cell requires another method to support proper initialization.

The tedium of creating cell subclasses and overriding four methods to support initialization led me to find a better solution to the problem.

What I do is swizzle the init method in an NSObject category and have it call a new method, which I called awake. The only problem with this is that it still doesn’t work for the NSCoding protocol (remember that initWithCoder: doesn’t call init). There’s actually a bunch of stuff that happens in nib loading that can result in odd things happening where initWithCoder: isn’t even called with the right object. Fortunately, though, the awakeAfterUsingCoder: method works for us in this situation:

+ (void)load {
    [self swizzle:@selector(init) with:@selector(swizzle_init)];
    [self swizzle:@selector(awakeAfterUsingCoder:) with:@selector(swizzle_awakeAfterUsingCoder:)];
}

- (void)awake {}

- (id)swizzle_awakeAfterUsingCoder:(NSCoder *)aDecoder {
    id result = [self swizzle_awakeAfterUsingCoder:aDecoder];
    [self awake];
    return result;
}

- (id)swizzle_init {
    self = [self swizzle_init];
    [self awake];
    return self;
}

Now, the cell class we want to write can look like this:

@interface MyCell : NSCell {
    id myArray;
}
@end

@implementation MyCell

- (void)awake {
    [super awake];
    myArray = [[NSArray alloc] init];
}
- (void)dealloc {
    [myArray release];
    [super dealloc];
}
@end

Using this technique allows you to more easily handle initialization in custom cell and view subclasses.