Cocoa for Scientists (Part XVII): Representing Data in Tables
Author: Drew McCormack
Website: http://www.maccoremac.com
In the last few tutorials we have dealt with representing data in 3D visualizations, using the Visualization Toolkit (VTK). In the next few tutorials I want to continue with the theme of data representation, starting this week with tabulated data, and progressing to 2D plotting in the following tutorials. To do this, we will develop a basic plotting application, capable of reading and writing data, presenting it in tabular form, and showing a simple plot.
Document-Based Applications
The application we are going to develop will be called ‘Spectra’. It will be capable of reading files containing two-column comma-separated values (CSV) text, presenting the data, and allowing the user to modify and store it.
To do all of this, we are going to make the app document-based. Document-based apps like Word, Excel, Keynote, and Pages can open and close their own documents, and have multiple documents open at one time. In this case, Spectra will be capable of having multiple plots open at once, each in a separate window.

You can create a Cocoa document-based application for Spectra as follows:
- Start Xcode.
- Choose File > New Project…
- In the Application group of the New Project assistant, choose Cocoa Document-based Application, and click the Next button.
- Enter ‘Spectra’ as the name of the project.
- Choose a folder to keep the project in, and click Finish.
View-Down Design
I find it’s often easiest when developing a Cocoa app to start by laying out the interface, and then working back from there to the controller and model layers. That’s how I’m going to do it this time.
- In Xcode, open the Spectra group in the Groups & Files source list on the left, then open the Resources group.
- Double click the MyDocument.nib bundle.
Now we need to make some changes to the document window.
- In Interface Builder, click on the text ‘Your Document contents here’, and press backspace to delete it.
- Select the Window icon in the MyDocument.nib window, and bring up the Inspector (Command-Shift I).
- Select the Attributes pane from the popup button in the Inspector.
- Check the ‘Has texture’ checkbox.
- Enter ‘Spectra’ for the Window title.
With the window ready to roll, we can now add our table.
- Select the Cocoa-Data palette in the IB palettes window. It is usually 5th from the left. (If you don’t see the palettes window, choose Tools > Palettes > Show Palettes.)
- Drag an
NSTableViewfrom the bottom right of the palette onto the document window. - Resize it to fit the window by dragging the corners, just like in a drawing application. Leave some space at the bottom for two buttons (see screenshot above).
Now to add those buttons:
- Open the Cocoa-Controls palette in the IB Palettes window. This is second from the left.
- Drag the textured button (second from right at the top) to the bottom of the document window. Double click and enter ‘Add’ as the button title.
- Repeat this for the ‘Remove’ button.
To give a name to each of the columns in the table, and to set the resizing behavior of the whole view, follow this procedure:
- Make sure the table is selected, and bring up the Inspector (Command-Shift I).
- Choose Attributes from the popup button in the Inspector.
- Select the first column of the table view by clicking on the column header. You may find you have to click a few times to drill down to the column. You will see that you have selected the column when the Inspector shows NSTableColumn as the title, and the column itself is highlighted.
- In the Inspector, enter ‘Energy’ as the Header Title.
- Repeat for the second column, but enter ‘Intensity’ as the title.
For the resizing behavior of the table view, we need to select it, and set some attributes in the Inspector. We actually change the resizing behavior of the NSScrollView that contains the NSTableView. When you insert a table view in Interface Builder, it automatically includes a scroll view so that you can scroll the contents of the table.
- Deselect the table view column so that the scroll view is selected. To do this, press the escape key a few times.
- Select Size from the Inspector popup button.
- Click on the Autosizing springs in the center of the box, so that they become curly. This indicates that the table view can resize with the window.

The Add and Remove buttons also need to be setup to behave properly when the window resizes. Here’s how you do that:
- Select the Add button, and in the Inspector popup button choose Size.
- Click the top and right springs on the outside of the box. They should change from straight to curly. This will cause the button to stay close to the bottom and left sides when the window is resized.
- Repeat for the Remove button.
To test the interface, select File > Test Interface. Try resizing the window, and make sure that everything resizes as you would expect. When you are satisfied, quit the test (Command-Q).
Bindings and NSArrayController
We’re going to use Cocoa bindings to populate the NSTableView with data; in particular, we will use an NSArrayController, which is designed especially for controlling arrays of objects, like those corresponding to the rows in our table.
To create an instance of NSArrayController
- Drag an
NSArrayControllerout of the Cocoa-Controllers palette in IB. This is the eighth palette, and has a cube icon. The array controller is bottom left. Drag it onto the Instances pane of the MyDocument.nib window. - Double click the name and rename the new instance ‘PointsArrayController’.
The content of PointsArrayController will be an array retrieved from the MyDocument instance. The MyDocument object is the nib file’s owner in this case, so we can bind the array controller to the File’s Owner instance.
- Select the PointsArrayController icon.
- Select Bindings in the Inspector popup button.
- Open the ‘contentArray’ binding by clicking the disclosure triangle.
- In the ‘Bind to’ popup button, choose File’s Owner (MyDocument).
- In the Model Key Path, enter ‘points’.
We haven’t added the points attribute to the MyDocument class yet, but we will do so shortly. It will be an array, with each element holding a dictionary. The array elements correspond to the rows of the table, with the columns corresponding to entries in the dictionaries.
Now to bind the two columns of the table view to the array controller.
- Select the Energy column first; remember, you probably need to click/double click several times to drill down to it. If you go too far, use escape to move up the view hierarchy.
- Select Bindings from the Inspector popup.
- Open the ‘value’ binding by clicking the disclosure triangle.
- In the Model Key Path text field, enter ‘energy’.
- Do same for the Intensity column, but enter ‘intensity’ as Model Key Path.
We have almost finished with IB; we just need to connect up those buttons.
- Control-drag from the Add button to the PointsArrayController icon in MyDocument.nib.
- In the Inspector, you should see the Target/Action tab appear in the Connections pane. Select
add:and press the Connect button. - Repeat for ‘Remove’, but select the
remove:action instead ofadd:.
It would be nice to have the Remove button disabled when there is no selection in the table view, or if there are no rows to remove. To achieve this, we can bind the button to some special keys in PointsArrayController.
- Select the Remove button, and in Inspector go to the Bindings pane.
- Open the ‘enabled’ binding.
- Bind to
PointsArrayController, and choose the Controller KeycanRemove. Leave the Model Key Path empty.
Save the IB document, and return to Xcode.
The MyDocument Class
We are finally ready for some source code. First we will edit the MyDocument class. If you recall, we will need a points attribute to hold the array of data, and because MyDocument is a document class, it will also need some methods to save and open files.
We’ll begin with the MyDocument.h file:
- Open the Classes group in Groups & Files.
- Drag the split up from the bottom of the right pane to reveal the source editor.
- Select MyDocument.h in Groups & Files.
Enter the following source
#import <Cocoa/Cocoa.h>
@interface MyDocument : NSDocument
{
NSArray *points;
}
-(NSArray *)points;
-(void)setPoints:(NSArray *)newPoints;
@end
Now switch to MyDocument.m. (To do this, you can click the little icon with overlapping gray and white squares, which is in the editor toolbar on the top-right.) Enter this source for MyDocument.m:
#import "MyDocument.h"
@implementation MyDocument
- (id)init
{
self = [super init];
if (self) {
[self setPoints:[NSArray array]];
}
return self;
}
- (void)dealloc
{
[self setPoints:nil];
[super dealloc];
}
- (NSArray *)points
{
return points;
}
- (void)setPoints:(NSArray *)newPoints
{
[newPoints retain];
[points release];
points = newPoints;
}
- (NSString *)windowNibName
{
return @"MyDocument";
}
- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName
error:(NSError **)outError
{
NSMutableString *csvString = [NSMutableString string];
NSEnumerator *pointEnum = [points objectEnumerator];
id point;
while ( point = [pointEnum nextObject] ) {
NSNumber *energy = [point valueForKey:@"energy"];
NSNumber *intensity = [point valueForKey:@"intensity"];
[csvString appendString:[NSString stringWithFormat:@"%@,%@\n",
energy, intensity]];
}
return [csvString writeToURL:absoluteURL atomically:NO
encoding:NSUTF8StringEncoding error:outError];
}
- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName
error:(NSError **)outError
{
NSString *fileString = [NSString stringWithContentsOfURL:absoluteURL
encoding:NSUTF8StringEncoding error:outError];
if ( nil == fileString ) return NO;
NSScanner *scanner = [NSScanner scannerWithString:fileString];
[scanner setCharactersToBeSkipped:
[NSCharacterSet characterSetWithCharactersInString:@"\n, "]];
NSMutableArray *newPoints = [NSMutableArray array];
float energy, intensity;
while ( [scanner scanFloat:&energy] && [scanner scanFloat:&intensity] ) {
[newPoints addObject:
[NSMutableDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithFloat:energy], @"energy",
[NSNumber numberWithFloat:intensity], @"intensity",
nil]];
}
[self setPoints:newPoints];
return YES;
}
@end
Most of these methods are self explanatory. There are a few different ways to read and write data in an NSDocument subclass such as MyDocument. The way I have chosen to do it here is to override the writeToURL:ofType:error: and readFromURL:ofType:error: methods. The format that I have decided to use is simply a comma-separated values (CSV) text format, with two columns.
The writeToURL:ofType:error: method uses a while loop over the elements of the points array, creating a string for each element that contains the CSV values for that row. The NSString method stringWithFormat: is used to take the NSNumbers in the points array, and convert them to string form. stringWithFormat: is similar to C’s printf, but has extra formatters, such as %@, which can be used for any object, and results in the the description method being used to determine the string to insert.
As is nearly always the case, reading is a little more complicated than writing. The readFromURL:ofType:error: method uses a class called NSScanner to extract the numbers from the CSV text. NSScanner performs a similar function to scanf in the standard C library. The scanFloat: method is invoked twice to scan the two numbers in each row. If either invocation fails, the method returns NO, and the while loop will exit. If the scan succeeds, the newly scanned numbers are added to an NSMutableDictionary, which in turn is added to the array used to hold points.
Our application class now has code to read and write documents, but how does the Finder know which documents should be opened by Spectra? For this, you need to change an entry in the Info.plist property list file. You could edit it directly, but it is better to follow this procedure:
- Open the Targets group in Groups & Files.
- Double click the Spectra target.
- Open the Properties tab.
- In the table at the bottom, double click the first row in the Extensions column, and replace the question marks with ‘spectra’.
- Close the target inspector.
Now, whenever a file has the extension ‘spectra’, Finder will pass it to Spectra to open, and when saving files, the extension ‘spectra’ will be added automatically to the file name.
Creating New Objects in PointsArrayController
There is one thing left to do before we can say stage one of Spectra is finished. When the user clicks the Add button, the action add: is invoked on the PointsArrayController instance. The idea is that PointsArrayController should create a new row of the table. The problem is, PointsArrayController doesn’t know anything about what is actually in the content array, so it doesn’t know that it needs to create entries for energy and intensity.
There are two ways we could remedy this. One would be to create a model class to store each energy–intensity pair, replacing the NSMutableDictionary instances that we have been using. The approach we will take here is to subclass NSArrayController, and override the newObject method, which is invoked to create the new object in the add: method. We will initialize an NSMutableDictionary instance in newObject with the appropriate keys.
- Select the Classes group in Groups & Files.
- Choose File > New File…
- In the Cocoa group of the New File sheet, choose Objective-C class, and click Next.
- Enter the file name PointsArrayController.m
- Click the Finish button.
Now add this code to the PointsArrayController.h file
#import <Cocoa/Cocoa.h>
@interface PointsArrayController : NSArrayController {
}
@end
and in PointsArrayController.m enter
#import "PointsArrayController.h"
@implementation PointsArrayController
-(id)newObject
{
return [[NSMutableDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithFloat:0.0], @"energy",
[NSNumber numberWithFloat:0.0], @"intensity",
nil] retain];
}
@end
Note that newObject is supposed to return an object with retain count 1, which explains the retain call. This is different to most so-called factory methods, which return autoreleased objects. The convention is that if ‘new’ appears in the method name, the new object is not autoreleased.
Now we need to make sure that IB knows about this new controller class, and that the instance in MyDocument.nib has the right class.
- Double click MyDocument.nib in Groups & Files.
- Drag PointsArrayController.h from Xcode onto the MyDocument.nib window in IB.
- In IB, click the Instances tab of the MyDocument.nib window, and click on the PointsArrayController icon.
- Bring up the Inspector with Command-Shift I.
- Choose Custom Class from popup button.
- Select the PointsArrayController class.
- Save your changes in IB.
Trying it Out
To test it out, build the project by clicking Build and Go in the toolbar of Xcode. If you just want to download the project, rather than typing it over, you can get it here.
With Spectra open, here are a few things to test out:
- Create a text file using a text editor, and give it the extension ‘spectra’. Enter two columns of CSV data and save. Double click the new file. It should open in Spectra.
- Create a new document in Spectra, and click the Add and Remove buttons to add and remove rows of data. Double click individual cells to change a values.
- Choose Save to save the data you have entered. Open the saved file in a text editor to confirm that the data is there, and is correct.
Cocoa gives you a lot of useful stuff, almost for free. With very little source code, Spectra has all the mechanics to open and save documents. When we add plotting in the next tutorial, we will have a created a document-based charting application in just a couple of hundred lines of code, with data entry and editing capabilities, and reading and writing in CSV format. Not bad going. Until then…



Comments
Great Tutorial
I was working on a similar project a few months ago and scoured the internet to get the NSScanner to read a csv file correctly and then pass it on with bindings to a NSTableview. Your tutorial is the first one I've seen that makes it as simple as I knew it must be. Great job, keep these awesome tutorials coming.
-Aaron
header
A very helpful tutorial. This is just what I needed.
In order to get it to compile, I had to put a declaration of the variable 'points' (NSArray *points;) into the MyDocument.h file.
Adam
Re: header
Don't know how that got removed. I've added it back in.
Thanks for the heads-up.
Drew
---------------------------
Drew McCormack
http://www.maccoremac.com
http://www.macanics.net
http://www.macresearch.org
drew, I think your tutorials
drew,
I think your tutorials are great. I enjoy reading and codin' them. But, I got a question for you, hope you could answer.
I want to use 'applicationShouldOpenUntitledFile' but seems to not be working. Here is what I did:
- created a new Objective-C class myController (.m & .h)
- Implemented the following code in myController.m
- (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
{
return NO;
}
- loaded the myController.h into the MainMenu.nib
but when I complie the app, the untitled doc appears. do you know what I migth be doing wrong.
applicationShouldOpenUntitledFile:
Did you set the controller object as your application's delegate? You need to do that.
Drew
---------------------------
Drew McCormack
http://www.maccoremac.com
http://www.macanics.net
http://www.macresearch.org
RE:applicationShouldOpenUntitledFile:
I knew I was missing something. thats for the tip....
Thanks allot
Thanks allot Drew for the outstanding tutorial. GOD bless you man!
Please keep up the cool stuff.
Youldash
================
http://www.youldash.net