Tag Archives: NSTableView

How to Make a Very Fast TableView on Mac with Variant Cell Height

I recently started my project to build my own Sina Weibo (a Chinese Twitter clone) app for the Mac. I started with NSTableView just like how I usually deal with UITableView on iOS programming, but I finally found that NSTableView is very complex and it’s completely different with UITableView, especially for custom cells and variant cell height. I have to use NSCell subclass instead of NSView subclass in NSTableView. Luckily, I then found an article Making Cocoa List Views Really Fast which talks about the performance issue about NSTableView. The author of the article wrote his own implementation of NSTableView, just like a UITableViewPXListView.

The benefit of using PXListView is that I can finally use NSViews as my cell instead of NSCells, so that you can construct your cells very easily just like how you make a custom UITableViewCell on iOS. However, I still have many issues when using this framework. Here is the thing:

My app is a Twitter-like app, so I need to show the Tweet and the Retweet in one cell, and the height of my cell is some paddings + Tweet NSTextView height + Retweet NSTextView height, which is based on the width of my PXListView, that is, when I changed the width of my PXListView, the height of each cell will be changed.

I then found the NS(Attributed)String+Geometrics category by Sheep Systems, which solved some of my problems. But it is way too slow because I have to calculate the height of NSTextView twice – in PXListView delegate listView:heightOfRow and my custom cell’s drawRect method.

After I looked closely through the category by Sheep Systems, I found that it used NSTextContainer, NSTextStorage, NSLayoutManager to calculate the height of the NSTextView with a fixed width. Every time the method was called, I have to alloc, init, and finally release them. It was just a waste of memory and time. I then suddenly found that all the three necessary classes to calculate the text height are just right in an NSTextView instance. So I rewrote the category to this:

@implementation NSTextView (Geometrics)

- (CGFloat)heightForWidth {

    if ([[self textStorage] length] > 0) {

		// Checking for empty string is necessary since Layout Manager will give the nominal
		// height of one line if length is 0.  Our API specifies 0.0 for an empty string.

		// NSLayoutManager is lazy, so we need the following kludge to force layout:
		[self.layoutManager glyphRangeForTextContainer:self.textContainer];

		return [self.layoutManager usedRectForTextContainer:self.textContainer].size.height;
	}

	return 0;
}

@end

It’s very clean now and have a great performance. Another thing is that I still have to call it 2 times when the cell is scrolling. To solve this, I wrote a TextViewHeightCache, which will cache the height of the given text with a fixed width. Next time PXListView asked for the height, the TextViewHeightCache first checked wether or not the height of the provided width is available in the cache. If we found it, just return the corresponding height, otherwise, recalculate it.

#import "NSTextView+Geometrics.h"

@implementation SNTextViewHeightCache

- (id)init {

	if ((self = [super init])) {
		textView = [[NSTextView alloc] init];
		textViewSizes = [[NSMutableDictionary alloc] init];
	}

	return self;
}

- (CGFloat)heightForText:(NSAttributedString *)text withWidth:(CGFloat)width {

	NSValue *sizeValue = [textViewSizes objectForKey:text];
	if (sizeValue) {
		NSSize size = [sizeValue sizeValue];
		if (size.width == width) {
			return size.height;
		}
	}

	[[textView textStorage] setAttributedString:text];
	[textView setFrameSize:CGSizeMake(width, 0)];

	CGFloat height = [textView heightForWidth];

	[textViewSizes setObject:[NSValue valueWithSize:CGSizeMake(width, height)] forKey:text];

	return height;
}

- (void)dealloc {

	[textView release];
	[textViewSizes release];

	[super dealloc];
}

@end

Now we have a very fast table view with variant cell height, completely without any knowledge of NSTableView.