mikeash.com: just this guy, you know?

Posted at 2006-04-06 00:00 | RSS feed (Full text feed) | Blog Index
Next article: Making Xcode Better
Previous article: Cocoa SIMBL Plugins
Tags: hack nib nscell
Custom NSCells Done Right
by Mike Ash  

Anyone who's done enough Cocoa has eventually run into the nightmare that is subclassing an NSCell. While it looks simple enough, actually getting an Interface Builder-generated control to use your NSCell subclass is effectively impossible. You either have to use CustomViews in IB, write an IBPalette, or do a whole lot of tedious and error-prone manual copying of attributes to get everything from the IB-provided cell into your own.

Tonight I'm going to show you a simple class you can add to your project which will make everything Just Work.

NSControl has a very nice +cellClass method. You override that, and NSControl will automatically use your cell class when creating a new control. The problem arises because Interface Builder archives everything into your nib. When the nib is unarchived, you get whatever cell was archived into it. The problem is that you don't get your custom cell, you get the NSCell, even though you set a custom NSControl subclass. IB does not see your +cellClass override.

The solution is to take advantage of the coder and force it to do our will. NSKeyedUnrchiver, used to unarchive all 10.2+ nibs, has this useful method: -setClass:forClassName:. This lets you tell the unarchiver to hand back a different class than it stored. As you've probably guessed already, we're going to use this to make it give us an instance of our subclass.

You could stick a bit of code like this in your NSControl's -initWithCoder: method, but I'm aiming higher than that. I want to make this work right for all of your subclasses, so you don't have to include extra code. You just do the obvious (override +cellClass) and it works.

To that end, we're going to subclass NSControl and then poseAsClass. We'll override -initWithCoder: to do our magic in a generic way, and then life is good.

We have to be careful, though, not to use the magic code in every circumstance. First, we'll fail gracefully on non-keyed coders. Then we want to make sure we're an actual NSControl subclass, so that we don't go mucking around with raw unsubclassed NSControls. We need to make sure that our superclass actually defines a cell class to begin with. And then we only need to perform the substitution if our cell class is different from super's. We'll stick these four conditions at the beginning of our method:

	BOOL sub = YES;
	
	sub = sub && [origCoder isKindOfClass: [NSKeyedUnarchiver class]]; // no support for 10.1 nibs
	sub = sub && ![self isMemberOfClass: [NSControl class]]; // no raw NSControls
	sub = sub && [[self superclass] cellClass] != nil; // need to have something to substitute
	sub = sub && [[self superclass] cellClass] != [[self class] cellClass]; // pointless if same

If any of those gives us NO then we just call super and we're done.

Assuming we pass all of the tests, then we get down to business. In case this archiver is used repeatedly, we want to make sure that our substitution is only in effect for us. We'll do that by getting the old class and saving it, then restoring it when we're finished decoding. Then we just substitute and call super. Here is the complete method:

- initWithCoder: (NSCoder *)origCoder
{
	BOOL sub = YES;
	
	sub = sub && [origCoder isKindOfClass: [NSKeyedUnarchiver class]]; // no support for 10.1 nibs
	sub = sub && ![self isMemberOfClass: [NSControl class]]; // no raw NSControls
	sub = sub && [[self superclass] cellClass] != nil; // need to have something to substitute
	sub = sub && [[self superclass] cellClass] != [[self class] cellClass]; // pointless if same
	
	if( !sub )
	{
		self = [super initWithCoder: origCoder]; 
	}
	else
	{
		NSKeyedUnarchiver *coder = (id)origCoder;
		
		// gather info about the superclass's cell and save the archiver's old mapping
		Class superCell = [[self superclass] cellClass];
		NSString *oldClassName = NSStringFromClass( superCell );
		Class oldClass = [coder classForClassName: oldClassName];
		if( !oldClass )
			oldClass = superCell;
		
		// override what comes out of the unarchiver
		[coder setClass: [[self class] cellClass] forClassName: oldClassName];
		
		// unarchive
		self = [super initWithCoder: coder];
		
		// set it back
		[coder setClass: oldClass forClassName: oldClassName];
	}
	
	return self;
}

Now we just have to make sure our class poses as NSControl:

+ (void)load
{
	[self poseAsClass: [NSControl class]];
}

Stick this all in an NSControl subclass called FixedNSControl (or whatever you prefer) and you're all set.

To test this, I created a very simple NSTextFiled subclass which basically does nothing but override +cellClass. It also overrides -drawRect: just to call super, so I have a convenient place to put a breakpoint. Here is MyTextField's source:

@interface MyTextFieldCell : NSTextFieldCell {} @end
@implementation MyTextFieldCell @end

@implementation MyTextField

+ (Class)cellClass
{
	return [MyTextFieldCell class];
}

- (void)drawRect: (NSRect)r
{
	[super drawRect: r];
}

@end

I put a breakpoint on drawRect: and tested the control's cell. Sure enough, it was my custom subclass:

(gdb) po [self cell]
<MyTextFieldCell: 0x32ed90>

Success! Now I can easily subclass NSControl and my custom cell follows right along, exactly as it always should have been.

Note: this code has not been thoroughly tested, use at your own risk, we are not responsible for any supernatural events arising due to its use, etc. etc.

Did you enjoy this article? I'm selling a whole book full of them. It's available for iBooks and Kindle, plus a direct download in PDF and ePub format. It's also available in paper for the old-fashioned. Click here for more information.

Comments:

Robert McCullough at 2006-06-15 17:08:00:
Hi Mike,

Great class! I’m having to create a bunch of custom controls and this was exactly what I’m looking for. I found a small problem with NSComboBoxes though. Your initWithCoder method was substituting a NSComboBoxCell class for a NSTextFieldCell class in a combo box object (which led to some pretty strange looking combo boxes).

I was able to solve the problem by adding this additional check to initWithCoder:

sub = sub && ([NSStringFromClass( [[self class] cellClass]) hasPrefix:@”NS”] == NO);

This assumes that we only want to make the substitution with one of our custom cell classes (which shouldn’t start with “NS”;).
– Rob

Daniel Dickison at 2006-09-13 22:31:00:
Wow, thank you thank you thank you! Today I attempted to subclass NSButtonCell for the first time, and wasted a good many hours before I found your solution. I can’t believe Cocoa makes this task so difficult…

Daniel Dickison at 2006-09-14 04:03:00:
I found a minor bug in this code: if you are using a custom sub-sub-class of a NSControl, then the following line doesn’t work:
sub = sub && [[self superclass] cellClass] != [[self class] cellClass]; // pointless if same

I posted a fix along with a demo project here

Jop van Heesch at 2006-10-22 11:38:00:
Maybe I am missing the point, but why not do the following?

If you want to use your own control: call setCellClass: in the control’s initialize method.
if you use a control declared in IB: set the cell in awakeFromNib, e.g. with NSTableColumn’s setDataCell:

mikeash at 2006-10-23 05:22:00:
Your specific examples work, but you can’t generalize them. For example, using setCellClass: on a subclassed NSButton that’s created from a regular NSButton in IB will do absolutely nothing at all. Using custom cells in tables has always been easy, the trouble is using custom cells in controls that are not so enlightened.

dude at 2007-01-13 08:12:00:
“Wow, thank you thank you thank you! Today I attempted to subclass NSButtonCell for the first time, and wasted a good many hours before I found your solution. I canÂ’t believe Cocoa makes this task so difficultÂ…”

Cocoa doesn’t make it difficult; this is a deficiency in Interface Builder.

mikeash at 2007-01-14 03:44:00:
As Interface Builder is part of the Cocoa development system, it’s entirely fair to state that Cocoa makes it difficult.

dude at 2007-01-16 17:00:00:
That’s like saying CSS and HTML suck because Notepad.exe blows. Don’t blame the API when it’s the tool that’s broken. You do understand the difference, right?

mikeash at 2007-01-17 20:43:00:
You do understand the difference between an open standard with a human-readable format and a closed standard that is intimately tied to a single application framework, right?

Call me back when there’s a nib editor other than Interface Builder, or when Cocoa offers another format for interface layout than nibs. IB has been an integral part of Cocoa development since it was NeXTSTEP. The situation is so hugely different from the HTML/Notepad situation that your analogy cannot possibly be thought to have any relevance.

Dave MacLachlan at 2007-04-18 17:13:00:
Just as a note:

We were using a similar method, and ran into a very interesting issue. If you poseAsClass for NSControl, and you launch your app from the postflight stage of an installer, you will not be able to paste into any text fields. Strange but true. Remove the poseAsClass and everything works fine. Launch your app from the Finder and it’s all good. Logged as radar: 5142672 Installer/PoseAs combo cause paste not to work

Hendo at 2007-08-12 13:33:00:
Hi, I’m new to Obj-C, and I just need a little help understanding how to use this. I went into InterfaceBuilder and created a subclass of NSControl called FixedNSControl. I assume I was then supposed to add the .m and .h files to my project? If thats correct what should the code look like in each file? Thanks!
– Hendo

Aaron Smith at 2010-05-05 19:30:58:
here's an updated version that will work with new 10.X SDK's..

Put this code in a category addition to NSControl


@implementation NSControl (Additions)

+ (void) initialize {
    [[[self class] superclass] initialize];
    Class NSControlClass = [NSControl class];
    method_exchangeImplementations(class_getInstanceMethod(NSControlClass,@selector(initWithCoder:)),class_getInstanceMethod(NSControlClass,@selector(newInitWithCoder:)));
}

//inspired from: [h]ttp://www.mikeash.com/pyblog/custom-nscells-done-right.html
- (id) newInitWithCoder:(NSCoder *) _coder {
    if(![_coder isKindOfClass:[NSKeyedUnarchiver class]]) return [self newInitWithCoder:_coder];
    NSKeyedUnarchiver * unarchiver = (NSKeyedUnarchiver *)_coder;
    Class supercell = [[self superclass] cellClass];
    Class selfcell = [[self class] cellClass];
    if(!selfcell || !supercell) return [self newInitWithCoder:_coder];
    NSString * supercellName = NSStringFromClass(supercell);
    [unarchiver setClass:selfcell forClassName:supercellName];
    self = [self newInitWithCoder:_coder];
    [unarchiver setClass:supercell forClassName:supercellName];
    return self;
}

@end

Mark Aufflick at 2014-05-23 06:37:52:
@Aaron you're going to want a dispatch_once wrapped around that swizzling code in initialize. You also don't need to call initialize on super, the runtime will call it on all classes/subclasses as per the docs.

k06a at 2015-07-23 12:30:48:
Please read doc carefully: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ControlCell/Tasks/SubclassingNSControl.html#//apple_ref/doc/uid/20000730

Note that overriding the cellClass class method of NSControl does not change the class of a cell object unarchived from a nib file.

k06a at 2015-08-16 05:25:07:
Aaron Smith, nice implementation, improved it a little bit to work with sub-sub-classes:
https://gist.github.com/k06a/3e8703538d9c4063cf04


Comments RSS feed for this page

Add your thoughts, post a comment:

Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.

Name:
Web site:
Comment:
Formatting: <i> <b> <blockquote> <code>. URLs are automatically hyperlinked.
Code syntax highlighting thanks to Pygments.
Hosted at DigitalOcean.