It seems that many of the apps in use today rely on or display feeds or lists of data; be that a Twitter timeline, an RSS feed or a list of history in a version control system, there’s a common trend in UI for using lists.
But displaying lists in applications can be much more than trivial, certainly for applications on Mac OS X, especially when you have to deal with large quantities of data, and have to solve problems such as variable row heights or changes to this information.
Ever since using Tweetie, a Twitter client for Mac OS X, from a development point of view I was always curious as to how the list view for the Twitter timeline was implemented; hundreds of rows of tweets can be displayed in this control, and the performance hit appears to be minimal; scrolling is smooth, there doesn’t appear to be a huge memory footprint. So how is it done?
Lists in Tweetie
If you class-dump Tweetie, you can see three classes at work:
- ABScrollView: A subclass of
NSScrollView
. It’s not clear as to why there is this subclass but anABColor
object is used for the background (as opposed to anNSColor
). - ABTableView: A subclass of
ABScrollView
which is used as the list control. - ABTableViewCell: The cell class used to display each row in
ABTableView
.
The interesting part of the class hierarchy is contained in ABTableView
and ABTableViewCell
. ABTableView
is a subclass of ABScrollView
(not NSTableView
) and is a control which can display a list of ABTableViewCell
s. However the real misnomer here is the cell part, because ABTableViewCell
is a subclass of NSView
, and not NSCell
.
So essentially what we have here is an implementation of something similar to NSCollectionView
, which has a set of subviews which each represent a row and have their own place in the view hierarchy. But how, then, is the Tweetie list view so performant?
@interface ABTableView : ABScrollView { ... NSMutableDictionary *reusableTableCells; ... } ... - (id)reusableCellsArrayForIdentifier:(id)arg1; - (id)dequeueReusableCellWithIdentifier:(id)arg1; ... @end
Reusing Cells
The answer seems to lie in a method declared on ABTableView
, which is -dequeueReusableCellWithIdentifier:
. For those of you who are iPhone developers, you will be familiar with a method on UITableView
with exactly the same method signature.
For those who aren’t, I shall explain: certainly compared to simple NSCell
instances, NSView
instances are pretty expensive. In fact, NSCell
is actually designed as a “mechanism for displaying text or images in an NSView without the overhead of a full NSView subclass”1. In light of this, if we have 100, 1,000 – or even more – rows in a list, we don’t want the overhead of 1,000 instances of NSView
, each used to represent a row. What’s more is that most of these cached views are wasted, since 1,000 rows cannot be displayed in the viewport at the same time. The iPhone takes advantage of this, and ensures that there are only enough view (in this case UIView
) instances to cover the height of the table view; if the associated rows are scrolled off screen, then the views are removed from the view hierarchy.
Scrolling works much like laying down track for a train by picking up track from behind it; when a row is scrolled so that it is no longer visible, it is removed from the table view and “enqueued” (ie stored in an array); when a row that is not visible is scrolled so that it becomes visible, the data source should first check to see if there are any enqueued views, and if so it is then added to the view hierarchy and displayed.
What this means is that hundreds of rows can be displayed in one control without a large performance hit, and is one of the optimizations that makes the iPhone UI so responsive.
Lists in Echofon
After class-dumping, you can see that Echofon implements their list views in pretty much the same way as Tweetie, using two main classes:
- FastTableView: A subclass of
NSScrollView
which is the list control. - FastTableCell: A subclass of
NSView
which represents a cell used for the table view.
Although not as explicit as the enquing/dequeuing in Tweetie’s list control, it would seem that FastTableView
does a similar job as it has declared on it -popCellForRow:fromArray:
and -popSpareCell
.
Also to note is the use of NSControl
and not NSView
as the superclass of the table “cell” object, unlike with Tweetie. I would guess that this is for some of the additions that NSControl
provides, perhaps the target/action mechanism.
Final word
When displaying large amounts of data in a list, as many applications do, often NSTableView
is not enough. It is a pretty performant class, but when you start doing things with variable row heights, it isn’t really cut for the job. Nor is a class such as NSCollectionView
; again it doesn’t implement variable row heights and lots of items can slow it down.
It seems that using view objects for each row, which have an established presence in the view hierarchy is the way to go, if you have the optimizations on top of this. For one you get the added convenience of using instances of NSView
, and not simply working with NSCell
.
As a final note, a quote I recall from Marcus Zarra‘s book on Core Data says:
The lesson I took away from that story is to expect my users to put thousands of times as much data into my application as I would ever consider reasonable
When working with large quantities of data, it is these kinds of optimizations that make your app more responsive and provide a better experience to the user.
Comments — 6
May 21, 2010
Very interesting! Tweetie probably uses that optimization combined with CoreGraphics pre-blending to achieve such smooth scrolling: http://blog.atebits.com/2008/12/fast-scrolling-in-tweetie-with-uitableview/.
Jun 1, 2010
From what I understand, and have heard from colleagues is that Loren Brichter used to work at Apple and worked on UIKit. I imagine he’s just used what he knows from the UITableView and UITableViewCell implementations and reimplemented them for AppKit.
Jun 1, 2010
@James: That would make a lot of sense – as you can see the class-dumped ABTableView and ABTableViewCell classes are very similar to UITableView and Cell; FastTableView and Cell are pretty similar but look less cloned.
Jun 25, 2010
FYI : I think the ABScrollView exists because of the custom NSScroller
Jun 26, 2010
@Michael: Perhaps; although NSScrollView lets you set custom horizontal and vertical scrollers through public methods.
My guess would be that it does something interesting in -setBackgroundABColor: or something with scrolling as -contentViewBoundsDidChangeNotification: is implemented. It also implements -scrollToRect:animated: and -scrollToPoint:animated: which are custom (presumably convenience) methods.
Apr 3, 2011
Alex, nice work firstly. I had been searching for this topic for a while and your post solved my problem finally.
A little question though. Is there any reason why you don’t declare the -visibleCellForRow in the header? I assume you don’t want it to be ‘public’ however in my application, I kinda need that. I want to change the height of the cell according to the text view’s height inside it, without the cell in – (CGFloat)listView:(PXListView*)aListView heightOfRow:(NSUInteger)row, this looks impossible to me.
Add comment