Posts tagged ‘NSTableView’

Multiline cells in NSTableView

When you’re building an application that is based on data records, and each record contains a significant amount of data that you want to display in a table row, the space per row may be too small to fit everything. You can add every data element in its own column, but this leads to horizontal scrolling which can be cumbersome. A better solution to this problem is combining multiple data elements in a single cell. I’ll demonstrate how this can be done with a core data based application using data binding through a basic example. The end result of the finalized sample will look like this:


MultilineCell Sample App

For the sample I have made a core data entity named Order with 2 real attributes, orderNumber and orderLine. To combine these two elements into a single cell I add another transient attribute to the entity, named orderNumberLine.


Datamodel.png

Because the attribute is transient it will not become part of a file when you save the model. This is fine because whenever we need the value for the orderNumberLine attribute we will compose it from the orderNumber and orderLine values.

The entity is linked to the class Order, in which we override the standard key-value method orderNumberLine:


// Compose an attributed string that combines the values of the
// orderNumber and orderLine attributes.
- (NSString*)orderNumberLine
{
	NSString *tmpValue;
	NSMutableAttributedString *attrValue;
	NSDictionary *boldFont =
		[NSDictionary dictionaryWithObject:[NSFont boldSystemFontOfSize:10.0]
					forKey:NSFontAttributeName];
	NSDictionary *normalFont =
		[NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:10.0]
					forKey:NSFontAttributeName];

	[self willAccessValueForKey:@"orderNumberLine"];
	// Put the order number in bold
	attrValue = [[NSMutableAttributedString alloc]
				initWithString:(self.orderNumber?self.orderNumber:@"")
				attributes:boldFont];
	// Then add a newline
	[attrValue appendAttributedString:[[NSAttributedString alloc]
				initWithString:@"\n"]];
	// And finally add the order line in normal font
	[attrValue appendAttributedString:[[NSAttributedString alloc]
				initWithString:(self.orderLine?[self.orderLine stringValue]:@"")
				attributes:normalFont]];

	tmpValue = [[NSAttributedString alloc] initWithAttributedString:attrValue];
	[self didAccessValueForKey:@"orderNumberLine"];

	return tmpValue;
}

The orderNumberLine method will compose a NSAttributedString that consist of the orderNumber and orderLine, separated by a newline. Because we’re using an attributed string we can also use different fonts for different elements, here the orderNumber is in 10 point bold, while the orderLine is in 10 point regular.

We can now bind the orderNumberLine value to a cell in an NSTableView in Interface Builder, then the cell will show a 2 line value. To properly display you need to increase the row height of the table view (in this case for a 10 point font size it to 26).

When you build the application adding only the orderNumberLine method to the Order class, you will notice that the cell that is bound to this method will display properly when it is redrawn (for example through scrolling or selecting the row). But when the value of either orderNumber or orderLine is changed elsewhere in the interface, the cell containing the composite string will not automatically redraw. This can easily be solved by doing an update to orderNumberLine attribute whenever one of its components (orderNumber or orderLine) changes. Then the bindings will take care that the UI is updated immediately. It doesn’t matter what you set the value of orderNumberLine to because it will be calculated upon retrieval anyway. The two following two methods demonstrate this:


// Override to trigger update of transient attribute orderNumberLine
- (void)setOrderNumber:(NSString*)value
{
	[self willChangeValueForKey:@"orderNumber"];
	[self setPrimitiveValue:value forKey: @"orderNumber"];
	[self didChangeValueForKey:@"orderNumber"];

	// Set the orderNumberLine to trigger updates to bound UI elements
	// The value we set it do doesn't matter as the actual value
	// is calculated whenever the orderNumberLine is fetched.
	self.orderNumberLine = @"";
}

// Override to trigger update of transient attribute orderNumberLine
- (void)setOrderLine:(NSNumber*)value
{
	[self willChangeValueForKey:@"orderLine"];
	[self setPrimitiveValue:value forKey: @"orderLine"];
	[self didChangeValueForKey:@"orderLine"];

	// Set the orderNumberLine to trigger updates to bound UI elements
	// The value we set it do doesn't matter as the actual value
	// is calculated whenever the orderNumberLine is fetched.
	self.orderNumberLine = @"";
}

To conclude, by adding a transient attribute, and writing three methods we can display a composed string in a cell. As this string can be an attributed string, we can apply different formatting to the elements composing the string. To look at the complete example, download it
here and open it in XCode.