Skip to content

Custom Table Cells, Bindings and Core Data

2008 July 9

In this post we’ll walk through one way to use a custom cell to display multiple managed object values in a single table view cell using bindings. The obvious question here is how to bind the Value of the table column in Interface Builder to more than one Model Key Path from Core Data. You can’t, for example, type in a string of attributes like ‘firstName, lastName’ or anything like that to specify that multiple values be taken.

Again, this article takes the previous CoreDataMultiWin application (in its third incarnation with multiple windows, multiple managed object contexts and table view double clicks) as its basis. This time, the interface for the application should end up looking like this:

The application running showing a single custom cell with multiple Core Data values

– Edit – The example project for this post is available to download from here: coredatamultiwin4.zip.

Store some additional data to display

Before we go into the code to show the cell and its values, let’s make a change to the data model so that we have a little more information to display.

Open MyDocument.xcdatamodel in Xcode and add a third string attribute to the Person entity called ‘salary’. Note that in a real-world app, you’d most likely want to use a number attribute rather than a string here and perform all sorts of validation in the interface to check input; since we’re using this attribute only for this demonstration, however, we’ll leave it as a string so that the user can type in anything they want.

Now that we’ve sorted out the model, we need to change the interface accordingly.

Open the PersonWindow.nib Window in Interface Builder and add a labelled text field for the salary so it looks like this:

The layout for the revised Person Window

Bind the Value of the new text field to the Person ‘selection’ ‘salary’ model key path.

Next open AddEmployeeWindow.nib in Interface Builder and make the window look like this:

The layout for the revised Add Employee window

Again, bind the Value of the new text field to the New Employee ‘selection’ ‘salary’.

We need to make a change to the AddEmployeeController code in order that the salary value is copied across contexts so open up the AddEmployeeController.m file. Assuming you made the change to use a dictionary of keys for copying the new employee object as suggested in the article on multiple managed object contexts, simply change the line that makes the employeeKeys array to the following:

NSArray *employeeKeys = [[NSArray alloc] initWithObjects:@"firstName", @"lastName", @"department", @"salary", nil];

It’s as simple as adding in the additional key to this array to make our new salary attribute work.

Build and run the application to check that the salary attribute is copied across from the Add Employee window to the main window (you’ll need to open a display window for an employee to check the value).

A custom table view cell

Now that we’ve got some very slightly more interesting data stored, let’s create a custom table view cell to display it.

Create a new Cocoa class called EmployeeTableCell and change EmployeeTableCell.h so that our object inherits from NSCell rather than NSObject:

@interface EmployeeTableCell : NSCell {
 
}
 
@end

Now open EmployeeTableCell.m and implement a method drawWithFrame: inView: that will be called to display the custom cell:

- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
	NSPoint textPoint;
 
	NSString *helloString = @"Hello!";
 
	textPoint.x = cellFrame.origin.x + 1;
	textPoint.y = cellFrame.origin.y;
 
	NSDictionary *textAttributes = [NSDictionary dictionaryWithObjectsAndKeys: [NSColor blackColor], NSForegroundColorAttributeName, [NSFont systemFontOfSize:13], NSFontAttributeName, nil];
	[helloString drawAtPoint:textPoint withAttributes:textAttributes];
}

This method just displays the text “Hello!” inside the cell so that we can test our custom table cell is working.

Now we need to tell our table view to use the custom cell so open up MyDocument.h and add an IBOutlet for an NSTableColumn:

IBOutlet NSTableColumn *peopleTableColumn;

Open MyDocument.nib and connect this Interface Builder outlet to the first column in the people table view.

Back in Xcode, open MyDocument.m and #import the EmployeeTableCell.h header file. Find the windowControllerDidLoadNib: method and add the following after the call to super:

EmployeeTableCell *infoCell = [[EmployeeTableCell alloc] init];
[peopleTableColumn setDataCell:infoCell];

Here we allocate a single instance of our new EmployeeTableCell class and tell the table view column to use this cell to display its data.

If you build and run the application you’ll find that instead of displaying the firstName in the column, our Hello! message is now displayed.

Return now to the EmployeeTableCell.m file and change the drawWithFrame: inView: method to the following:

- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
	NSString *firstNameString = [self objectValue];
 
	NSPoint textPoint;
 
	textPoint.x = cellFrame.origin.x + 1;
	textPoint.y = cellFrame.origin.y;
 
	NSDictionary *textAttributes = [NSDictionary dictionaryWithObjectsAndKeys: [NSColor blackColor], NSForegroundColorAttributeName, [NSFont systemFontOfSize:13], NSFontAttributeName, nil];
	[firstNameString drawAtPoint:textPoint withAttributes:textAttributes];
}

This code pulls the firstName value from the object represented by the cell and then displays it instead of the Hello! string.

Again, build and run the application and you will see the first and last names displayed in the columns as they were originally but this time the first name is displayed for us by our custom cell.

Getting multiple values to a cell

Since we find out the string to display by getting the cell’s objectValue, in order to receive multiple values we obviously need to have the objectValue somehow return an array, dictionary or some such structure of information rather than just the one string. For this example, we’re going to generate a dictionary of Person information which we then pass to the cell.

At this point we need to make a custom NSManagedObject subclass for our Person entity so that we can generate the necessary dictionary of information on demand when the cell requires it.

Open the data model again and add to the Person entity an Undefined, Transient attribute called ‘personDictionary’. Set the class of the Person entity to PersonObject rather than NSManagedObject.

With the Person entity selected in the data model, make a new file in Xcode and selected the ‘Managed Object Class’ template in the ‘Design’ category. Click ‘Next’ to get to the ‘Managed Object Class Generation’ screen and make sure the Person entity is ticked along with the Generate accessors checkbox at the bottom of the screen. Deselect the ‘Generate Obj-C 2.0 Properties':

The Managed Object Class Generation Assistant in Xcode

Click ‘Finish’ and you’ll be greeted with an automatically-generated PersonObject.h file with declarations for the setters and getters.

Remove the setter for the personDictionary attribute and replace the UNKNOWN_TYPE with an NSDictionary pointer. Also add a class method employeeKeys so that your PersonObject declarations look like this:

@interface PersonObject :  NSManagedObject
{
}
 
+ (NSArray *)employeeKeys;
- (NSDictionary *)personDictionary;
 
- (NSString *)salary;
- (void)setSalary:(NSString *)value;
 
- (NSString *)firstName;
- (void)setFirstName:(NSString *)value;
 
- (NSString *)lastName;
- (void)setLastName:(NSString *)value;
 
- (NSManagedObject *)department;
- (void)setDepartment:(NSManagedObject *)value;
 
@end

Now open up PersonObject.m and again remove the setPersonDictionary: setter from the implementations.

We’ll implement the employeeKeys method to return the keys of the employee data (like the array we used in previous articles when copying managed objects from one context to another).

+ (NSArray *)employeeKeys
{
	static NSArray *employeeKeys = nil;
 
	if( employeeKeys == nil )
		employeeKeys = [[NSArray alloc] initWithObjects:@"firstName", @"lastName", @"department", @"salary", nil];
 
	return employeeKeys;
}

This class method maintains a static array of keys that can be used by other methods to build a dictionary.

Next change the getter method’s UNKNOWN_TYPE again and implement it like this:

- (NSDictionary *)personDictionary
{
    return [self dictionaryWithValuesForKeys:[[self class] employeeKeys]];
}

This very simple method simply returns a dictionary filled with the required keys and values.

Back in Interface Builder, change the binding for the table column to the ‘personDictionary’ key instead of the ‘firstName’ key.

Now that we’ve changed the object provided to the table column, we need to make changes to our custom cell to collect the data properly. Open EmployeeTableCell.m and change the method to the following:

- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
	NSDictionary *cellValues = [self objectValue];
 
	NSPoint textPoint;
 
	NSString *firstNameString = [cellValues valueForKey:@"firstName"];
 
	textPoint.x = cellFrame.origin.x + 1;
	textPoint.y = cellFrame.origin.y;
 
	NSDictionary *textAttributes = [NSDictionary dictionaryWithObjectsAndKeys: [NSColor blackColor], NSForegroundColorAttributeName, [NSFont systemFontOfSize:13], NSFontAttributeName, nil];
	[firstNameString drawAtPoint:textPoint withAttributes:textAttributes];
}

Now the firstName attribute is pulled from the dictionary and displayed as before.

Build and run the application to make sure all works as expected.

Now that we have the potential to display multiple values in the cell, return to Interface Builder and change the MyDocument.nib main Window so that there is only our custom cell table column in the table view, resized to fill the table. Change the title of the column to “Employees”. Check the ‘Alternating Rows’ checkbox for the table view and set its row height to 50:

The revised layout of the main window with our single custom cell column

Back in Xcode change our custom cell drawWithFrame: inView: method to the following:

- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
	NSDictionary *cellValues = [self objectValue];
 
	NSPoint textPoint;
 
	NSString *nameString = [cellValues valueForKey:@"firstName"];
	nameString = [nameString stringByAppendingFormat:@" %@", [cellValues valueForKey:@"lastName"]];
 
	NSString *salaryString = [cellValues valueForKey:@"salary"];
	NSString *departmentString = [NSString stringWithFormat:@"Department: %@", [[cellValues valueForKey:@"department"] valueForKey:@"name"]];
 
	textPoint.x = cellFrame.origin.x + 1;
	textPoint.y = cellFrame.origin.y;
 
	NSMutableDictionary *textAttributes = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSColor blackColor], NSForegroundColorAttributeName, [NSFont systemFontOfSize:13], NSFontAttributeName, nil];
	[nameString drawAtPoint:textPoint withAttributes:textAttributes];
 
	textPoint.y = cellFrame.origin.y + 18;
 
	[textAttributes setValue:[NSFont systemFontOfSize:15] forKey:NSFontAttributeName];
	[salaryString drawAtPoint:textPoint withAttributes:textAttributes];
 
	textPoint.y = cellFrame.origin.y + 36;
	[textAttributes setValue:[NSFont systemFontOfSize:11] forKey:NSFontAttributeName];
	[textAttributes setValue:[NSColor grayColor] forKey:NSForegroundColorAttributeName];
	[departmentString drawAtPoint:textPoint withAttributes:textAttributes];
}

This code builds and displays a nameString using the firstName and lastName keys from the dictionary. It also displays the salary and shows that we are able to display the department “name” from a managed object value for the key “department”.

Build and run the application to make sure the display works. You will, however, notice that if you open an employee window and change the salary, the main window doesn’t automatically update the employee cell. This is because the binding mechanism does not know what values affect the values in the personDictionary.

To fix this problem, add the following method to PersonObject.m:

+ (NSSet *)keyPathsForValuesAffectingPersonDictionary
{
	return [NSSet setWithObjects:@"firstName", @"lastName", @"department", @"salary", nil];
}

The Key-Value Coding mechanism allows for a class method that asks which values affect a dependent key such as our personDictionary; the name of the method must follow the format ‘keyPathsForValuesAffecting…’ with the attribute name starting with an initial capital letter.

If you now build and run the application, whenever a relevant attribute is changed in a Person Window, anything bound to the personDictionary key is told that the values have changed and here the cell in the main document window automatically updates to display the correct information.

Error Checking

None of the code here checks for potential errors. You’ll discover, for example, that creating a new employee and leaving the salary field blank will crash the application. You could fix this by making it a required field, or by using validation, or just by having any code that expects a salary value check that it hasn’t received a nil string. This goes for all the attributes in this example!

Share
9 Responses leave one →
  1. April 22, 2009

    Just what I was looking for, thank you Tim.

    A few of things I did differently are below. Two were because I’m not using Garbage Collection.

    1. Let the Managed Object Class Generator create Objective-C 2.0 properties so that there was less code around. I then simply changed the UNKNOWN_TYPE to NSDictionary and added the custom getter method.

    2. In the getter for the NSDictionary I generated an auto-releasing dictionary with an auto-releasing array:
    return [self dictionaryWithValuesForKeys:[NSArray arrayWithObjects:@”firstName”, @”lastName”, @”department”, @”salary”, nil]];

    3. In MyDocument.m I released infoCell:
    EmployeeTableCell *infoCell = [[EmployeeTableCell alloc] init];
    [peopleTableColumn setDataCell:infoCell];
    [infoCell release];

  2. Mark Knopper permalink
    May 11, 2009

    Thanks for this tutorial and example code. There are very few examples of how to implement transient properties out there – it was useful to see how you used keyPathsForValuesAffectingPersonDictionary.

  3. June 22, 2009

    Thank you Tim!

    Great tutorial. Just what I needed to kick start my custom TableView. Wish I had known about the Managed Object Class Generator before I hand-cranked four classes last week! Thanks also to Simon Wolf for his non-GC comments – I’m not using GC either (yet).

  4. Tim Costa permalink
    October 27, 2009

    Tim, thanks for your great tutorial! Using it I was able to add basic editing to my custom cell. I thought I would share with everyone else. It doesn’t move the editor to the proper location visually, but shows how to get the guts of editing working. Basically, it takes advantage of an NSFormatter subclass, and the fact that we are using dictionaries.

    1. I subclassed NSTextFieldCell, rather than NSCell. (I’m not sure yet how to do this with just NSCell. In my case I have a name, and some read only metadata about the object I am displaying, so this works fine for me.)

    2. Add the following to your NSManagedObject subclass (PersonObject in your example):

    -(void)setDictionary
    {
    [self setValuesForKeysWithDictionary:aDictionary];
    }

    3. Subclass NSFormatter with the following methods (these are the minimum required to subclass NSFormatter, according to the documentation)

    – (BOOL)getObjectValue:(id *)anObject forString:(NSString *)string errorDescription:(NSString **)error
    {
    if (anObject){
    *anObject = [NSDictionary dictionaryWithObject:string forKey:@”name”];
    return YES;
    }
    return NO; // This probably shouldn’t happen…Could be a bindings issue if it does?
    }
    – (NSString *)stringForObjectValue:(id)anObject
    {
    if (![anObject isKindOfClass:[NSDictionary class]])
    return nil;
    return [anObject valueForKey:@”name”]; // This will propagate back to your NSManagedObject subclass, since we created setDictionary.
    }
    – (NSAttributedString*)attributedStringForObjectValue:(id)anObject withDefaultAttributes:(NSDictionary *)attrs
    {
    if (![anObject isKindOfClass:[NSDictionary class]])
    return nil;
    NSAttributedString * anAttributedString = [[NSAttributedString alloc]initWithString: [anObject valueForKey:@”name”]];

    return anAttributedString;
    }

    4. Add the following to your NSTextFieldCell’s awakeFromNib:
    MyFormatter * aMyFormatter = [[[MyFormatter alloc]init]autorelease];
    [self setFormatter:aMyFormatter];

    That should be it, if I didn’t leave out any steps… I hope this helps someone, and if you spot anything that can be improved in my code, please post a followup!
    -Tim

  5. Tim Costa permalink
    October 27, 2009

    Sorry… in step 2 I put some angled brackets which got edited out by the board software.

    It should read -(void)set_YourClassName_Dictionary. Re-add the setter that you removed in Tim’s original tutorial.

  6. August 24, 2011

    Hi Tim,

    This is a great tutorial and the clearest I’ve found on this subject. So thank you for that. However! I am having a spot of difficulty despite believing I’ve followed the steps to the letter in my own project.

    I get up to the part where you bind to the personDictionary in Interface Builder. I’ve defined personDictionary both in my model and also added the various required functions to the custom managedObject class code. When I run the project, I get the following error and crash:

    [CustomCell setPlaceholderString:]: unrecognized selector sent to instance 0x100637f20

    If I unbind the personDictionary from the table column or change it back to the NSString firstName, the error goes away.

    Do you have any ideas what might be happening or pointers of where to look for problems. This has me stumped!

    Appreciate your time.

    Thanks,

    Gareth.

  7. Tim Isted permalink*
    August 25, 2011

    Hi Gareth,
    Have you set a placeholder string in the xib file? If so, just remove it and all will be fine. The custom cell inherits from NSCell rather than NSTextFieldCell. The placeholder string property is only available on NSTextFieldCell, hence the error you’re seeing.
    Tim

  8. August 25, 2011

    Hi Tim,

    Thanks for the speedy reply. Appreciate your help. I had thought the same thing, why it is trying to use a placeholder when I haven’t set one up and I’m using a NSCell? However there’s definitely no place holder set.

    To check I just added a brand new column to my table, set the binding to clientDictionary and all is ok. But then I change the NSTableColumn type to be my CustomCell class, and it crashes with the setPlaceHolderString error.

    It’s just too weird!

    Any other ideas?

    Thanks again.

  9. August 25, 2011

    Update on this. I’m using your Nib file and your EmployeeTableCell and I still get the damned -[EmployeeTableCell setPlaceholderString:]: unrecognized selector sent to instance 0x10016e1f0 message.

    The only thing I’m doing differently to yours is setting the custom cell in awakeFromNib, because I’m using a non document based app, so can’t use windowControllerDidLoadNib…

    – (void)awakeFromNib {
    // Set next responder for this view so we can intercept delete key presses
    [mainView setNextResponder:self];

    // Set up custom cell for table view
    EmployeeTableCell *statusCell = [[EmployeeTableCell alloc] init];
    [statusTableColumn setDataCell:statusCell];
    }

Leave a Reply

Note: You can use basic XHTML in your comments. Your email address will never be published.

Subscribe to this comment feed via RSS