A project I’m working on for OSX has a need for a table view that has sections. This is what UITableView is like, and sort of what NSOutlineView is supposed to be used for, but it seemed like overkill for a simple two level hierarchy, and is one of the hardest Cocoa classes to use, requiring a considerable amount of set up to make it work. I also wanted to use bindings, and NSTreeController is also not the nicest of classes.
My requirements were that the NSTableView needed to display N sections, and each section would be represented by an NSArray. As the table view wasn’t going to be the only view on the objects in the array, and I needed to be able to track selections in all the views, I wanted to use an NSArrayController, bindings and KVO.
As NSTableView has no support for section headers, I was going to have to present them as another row in the table, but styled differently. This meant that the data as seen by the NSTableView would have to be a composite array, a flattened version of the source arrays with the section headers added.

This composite array would be the content array of the NSArrayController but instead of creating a real array I decided to use a piece of the key value coding system that means an object can pretend to be an array. Using specially named methods, KVO can access the object like an array.
I created an object called FVCompositeArray with two properties:
@property (readwrite, strong) NSArray *arrays;
@property (readwrite, strong) NSArray *headers;
Arrays was the going to hold the source arrays, and headers was going to hold the section headers, one for each array in arrays.
I decided the key name for KVO would be ‘items’. This defines what our method names need to be called:
- (NSUInteger)countOfItems;
- (id)objectInItemsAtIndex: (NSUInteger)index;
- (void)insertObject: (id)object
inItemsAtIndex: (NSUInteger)index;
- (void)removeObjectFromItemsAtIndex: (NSUInteger)index;
The first two methods need to be implemented for both mutable and immutable bindings, the second two only need implemented for mutable. countOfItems needs to return the number of items in arrays plus the number of sections.
- (NSUInteger)countOfItems
{
NSUInteger totalCount = self.arrays.count;
for (NSArray *array in self.arrays) {
totalCount += array.count;
}
return totalCount;
}
For the composite array objectInItemsAtIndex needs to count through the arrays taking the headers into account to find the right array and index in that array. I wrote a helper method to do the bulk of this work, it will be useful for later methods. The helper method takes an index in the composite array, and walks through the array of arrays and returns the array it refers to, and the index in that array. If the index actually refers to one of the header items, it returns nil, and the indexInArray refers to the position in the headers array.
- (NSMutableArray *)arrayAtIndex: (NSUInteger)index indexInArray: (NSUInteger *)indexInArray
{
NSUInteger arrayIdx = 0;
NSMutableArray *currentArray = self.arrays[arrayIdx];
// If the index is 0 then we’re in a header.
if (index == 0) {
*indexInArray = 0;
return nil;
}
// Offset the first header
index -= 1;
// Find the array that the index is in.
while (index >= currentArray.count) {
// Offset to the next array
index -= (currentArray.count);
arrayIdx += 1;
// Again, if index is 0, then it’s a header.
if (index == 0) {
*indexInArray = arrayIdx;
return nil;
}
// Offset the header
index -= 1;
// Update the current array
currentArray = self.arrays[arrayIdx];
if (arrayIdx + 1 >= self.arrays.count) {
break;
}
}
*indexInArray = index;
return currentArray;
}
With this method to help, objectInItemsAtIndex becomes
- (id)objectInItemsAtIndex: (NSUInteger)index
{
NSUInteger indexInArray;
NSMutableArray *currentArray;
currentArray = [self arrayAtIndex:index indexInArray:&indexInArray];
// If currentArray is nil, then the index is a header
if (currentArray == nil) {
return self.headers[indexInArray];
}
return currentArray[indexInArray];
}
It may seem awkward that arrayAtIndex:indexInArray: is returning nil for the header, if it was to return the self.headers array, then the if statement in objectInItemsAtIndex: could be done away with, but as will become clear later, it is useful for some methods to know if an index refers to the header array.
With these methods, an NSArrayController bound to an NSTableView will be able to use the items key of FVCompositeArray as its contentArray.
The example project is set up simply with a single column NSTableView in a window, and the tableview’s Table Content binding is bound to the arrangedObjects property of an NSArrayController set up in the AppDelegate. The NSTableColumn’s Value binding is bound to the same array controller.
The source NSMutableArrays and FVCompositeArray are set up in the AppDelegate
- (void)applicationDidFinishLaunching: (NSNotification *)aNotification
{
_section1 = [NSMutableArray arrayWithArray:@[@”Test 1.1", @”Test 1.2", @”Test 1.3"]];
_section2 = [NSMutableArray arrayWithArray:@[@”Test 2.1", @”Test 2.2", @”Test 2.3", @”Test 2.4"]];
_section3 = [NSMutableArray arrayWithArray:@[@”Red”, @”Green”, @”Blue”]];
NSArray *headers = @[@”Section 1", @”Section 2", @”Colours”];
_model = [[FVCompositeArray alloc] init];
_model.arrays = @[_section1, _section2, _section3];
_model.headers = headers;
The NSArrayController’s contentArray property needs connected to the FVCompositeArray.
self.arrayController = [[NSArrayController alloc] init];[self.arrayController bind:@”contentArray” toObject:_model withKeyPath:@”items” options:nil];
And with this binding in place, running the example gives

When using an NSArrayController, adding/removing items is carried out using methods on the array controller, not the source arrays, so the extra KVO methods insertObject:inItemsAtIndex: and removeObjectFromItemsAtIndex: need to be implemented on the FVCompositeArray.
insertObject:inItemsAtIndex: needs to find the array that corresponds to index, and it needs to work out where to insert the object in that array. This is similar to what arrayAtIndex:indexInArray: did and it can be reused. The difference this time is when it returns a nil to represent that the index is a section header item it means that the object needs to be appended to the end of the previous array.
- (void)insertObject: (id)object inItemsAtIndex: (NSUInteger)index
{
NSUInteger indexInArray;
NSMutableArray *array = [self arrayAtIndex:index indexInArray:&indexInArray];
// As we can’t insert into a header
// when we get nil, we know it actually means we want to insert at the end of the
// previous array.
if (array == nil) {
array = self.arrays[indexInArray — 1];
indexInArray = array.count;
}
[array insertObject:object atIndex:indexInArray];
}
This is why the source arrays were originally created as NSMutableArray: so objects can be inserted in them. If the mutability wasn’t required, then they could be created as NSArray.
And similarly, the removeObjectFromItemsAtIndex: needs to find the array corresponding to index, and remove the object from it. This time, the nil being returned from arrayAtIndex:indexInArray: means the removeObjectAtIndex: message is sent to a nil object, so it does nothing.
- (void)removeObjectFromItemsAtIndex: (NSUInteger)index
{
NSUInteger indexInArray;
NSMutableArray *array = [self arrayAtIndex:index indexInArray:&indexInArray];
[array removeObjectAtIndex:indexInArray];
}
Adding/removing objects to the NSArrayController is still slightly awkward though, because the methods insertObject:atArrangedObjectIndex: and removeObject:atArrangedObjectIndex: take the index into the composite array, which isn’t generally what is wanted — the logic is that of inserting/removing an index in one of the source arrays, which then updates the composite array.
To accomplish this, another helper function to convert between an index in a source array and the index in the composite array is added to FVCompositeArray.
- (NSUInteger)compositeIndexForIndex: (NSUInteger)index inArray: (NSMutableArray *)array
{
NSUInteger compositeIndex = 0;
NSUInteger arrayIndex = 0;
NSArray *currentArray = self.arrays[arrayIndex++];
while (currentArray != array) {
compositeIndex += (1 + currentArray.count);
currentArray = self.arrays[arrayIndex++];
}
compositeIndex += (1 + index);
return compositeIndex;
}
With this an object can be added to an array like so
- (IBAction)addToSection1: (id)sender
{
NSUInteger compositeIndex = [_model compositeIndexForIndex:1 inArray:_section1];
[self.arrayController insertObject:@”Addition” atArrangedObjectIndex:compositeIndex];
}
The NSTableView now displays all the items in the source arrays and has the section headers between them, but they don’t look like section headers, they just look the same as the other rows. With some logic in the NSTableViewDelegate, a different NSTableCellView can be returned for the section header rows than the normal rows.
If a simple OTHeader object is used for the objects in the header array, for example
@interface OTHeader : NSObject
@property (readwrite, copy) NSString *headerTitle;
- (instancetype)initWithTitle☹NSString *)title;
@end
NSArray *headers = @[[[OTHeader alloc] initWithTitle:@”Section 1"],
[[OTHeader alloc] initWithTitle:@”Section 2"],
[[OTHeader alloc] initWithTitle:@”Colours”]];
then the logic could be something like
- (NSView *)tableView: (NSTableView *)tableView viewForTableColumn: (NSTableColumn *)tableColumn row: (NSInteger)row
{
id object = self.arrayController.arrangedObjects[row];
NSTableCellView *view;
if ([object isKindOfClass:[OTHeader class]]) {
view = [tableView makeViewWithIdentifier:@”HeaderRow” owner:self];
OTHeader *header = (OTHeader *)object;
view.textField.stringValue = header.headerTitle;
} else if ([object isKindOfClass:[NSString class]]) {
view = [tableView makeViewWithIdentifier:@”NormalRow” owner:self];
view.textField.stringValue = object;
}
return view;
}
and with some tweaks to the HeaderRow NSTableCellView in Interface Builder and the example now looks like

A few final tweaks that are needed: The section header rows can be selected, but by implementing tableView:shouldSelectRow:
- (BOOL)tableView: (NSTableView *)tableView shouldSelectRow: (NSInteger)row
{
id object = self.arrayController.arrangedObjects[row];
return ![object isKindOfClass:[OTHeader class]];
}
the section headers avoid being selected, in all but one case that I can find — if a selection is deleted from then the array controller by default will try to avoid an empty selection. Sometimes the selection it makes is a section header. The easiest way to avoid this though, is to set avoidsEmptySelection to NO.
self.arrayController.avoidsEmptySelection = NO;
With this, a very simple NSTableView can be created with section headers.
Code is available at GitHub: https://github.com/iainx/CompositeArrayController
Email me when Explorations in Cocoa publishes stories



