Life as Clay

Cocoa Programming for Mac OS X: Chapter 8, Challenge 2

with 3 comments


This challenge is simple — type in the code provided, add the bindings in Interface Builder, and go…  that is, until the “make sure to add sorting!” comment at the end.

Sorting the RaiseMan application that does not use NSArrayController requires only a careful reading of the “For the More Curious: Sorting without NSArrayController” section in Chapter 8. The key sentence here, which I missed several times, is, “An optional table view dataSource method is triggered when the user clicks the header of a column with a sort descriptor:

There are a couple of things to notice and do:

1. tableView:sortDescriptorsDidChange: is a delegate method… so this means that you have to set MyDocument to be the delegate of the table view. Presumably you already did that if you set up the bindings correctly.

2. You need to enter the code that appears just above the “Challenge 1” header on page 135 into MyDocument.m – this is the delegate method called when you click the column header.

3. You cannot use the delegate method code as it appears in the book if you entered the code in Challenge 2, because you have a pointer to an IBOutlet object called tableView and this delegate method waits for a call from the interface element, not from your pointer, if that makes sense. Think about it this way — the flow of information happens in two directions: that coming in from the interface to the method, and that going from the method back to the interface. You’ll receive a compile warning if you call it tableView. Change it to aTableView. (You probably noticed similar changes in the code under Challenge 2.)  When referring to the incoming messages, those should be called aTableView. When referring to outgoing messages, those should be tableView because that is what your IBOutlet pointer is called. Therefore, the final line, [tableView reloadData] needs to remain tableView and not change to aTableView because you now want to call on the IBOutlet pointer to send instructions back to the interface.

4. The line [myArray sortUsingDescriptors:newDescriptors]; needs to contain the name of your array… employees. It should read: [employees sortUsingDescriptors:newDescriptors]; so that it sorts your array.

The final point tripped me up longer than any of the rest of it because I thought that the purpose of the exercise was to avoid using key coding. I was wrong. The point of the exercise is to not use NSArrayController. To sort your columns, you have to have sort descriptors. Those are entered in the same spot as in the previous challenge and the chapter exercise. In other words, when you have the name column in the table selected, the Interface Builder Table Column Attributes should look like this:

The same discussion about compare: vs. localizedStandardCompare: is valid here, so you can change it to the latter if you don’t want Z to appear before a when you sort. Look here for more info. The full code follows the break:

Here are the Interface Builder connections:

Here’s the code:

Person.h

#import <Foundation/Foundation.h>

@interface Person : NSObject {
	NSString *personName;
	float expectedRaise;
}

@property (readwrite, copy) NSString *personName;
@property (readwrite) float expectedRaise;

@end

Person.m

#import "Person.h"


@implementation Person

- (id)init
{
	[super init];
	expectedRaise = 0.5;
	personName = @"New Person";
	return self;
}

- (void)dealloc
{
	[personName release];
	[super dealloc];
}

- (void)setNilValueForKey:(NSString *)key
{
	if ([key isEqual:@"expectedRaise"]) {
		[self setExpectedRaise:0.0];
	} else {
		[super setNilValueForKey:key];
	}
	
}

@synthesize personName;
@synthesize expectedRaise;

@end

MyDocument.h (Notice the important @class Person; line at the top… it won’t compile without it.)

#import <Cocoa/Cocoa.h>
@class Person;

@interface MyDocument : NSDocument
{
	NSMutableArray *employees;
	IBOutlet NSTableView *tableView;
}
- (IBAction)createEmployee:(id)sender;
- (IBAction)deleteSelectedEmployees:(id)sender;
@end

MyDocument.m (note: some of these methods automatically appear when you create a new project that uses the Document template… I left them alone.)

#import "MyDocument.h"

@implementation MyDocument

- (id)init
{
    self = [super init];
    if (self) {
		employees = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)dealloc
{
	[employees release];
	[super dealloc];
}

- (NSString *)windowNibName
{
    return @"MyDocument";
}

- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
    [super windowControllerDidLoadNib:aController];
}

- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError
{

    if ( outError != NULL ) {
		*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL];
	}
	return nil;
}

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError
{
   
    if ( outError != NULL ) {
		*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL];
	}
    return YES;
}

#pragma mark Action methods
- (IBAction)deleteSelectedEmployees:(id)sender
{
	// Which row is selected?
	NSIndexSet *rows = [tableView selectedRowIndexes];
	
	// Is the selection empty?
	if ([rows count] == 0) {
		NSBeep();
		return;
	}
	[employees removeObjectsAtIndexes:rows];
	[tableView reloadData];
}

- (IBAction)createEmployee:(id)sender
{
	Person *newEmployee = [[Person alloc] init];
	[employees addObject:newEmployee];
	[newEmployee release];
	[tableView reloadData];
}

#pragma mark Table view dataSource methods
- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
	return [employees count];
}

- (id)tableView:(NSTableView *)aTableView 
objectValueForTableColumn:(NSTableColumn *)aTableColumn 
			row:(int)rowIndex
{
	// What is the identifier for the column?
	NSString *identifier = [aTableColumn identifier];
	
	// What person?
	Person *person = [employees objectAtIndex:rowIndex];
	
	// What is the value of the attributenamed identifier?
	return [person valueForKey:identifier];
}

- (void)tableView:(NSTableView *)aTableView
   setObjectValue:(id)anObject
   forTableColumn:(NSTableColumn *)aTableColumn
			  row:(int)rowIndex
{
	NSString *identifier = [aTableColumn identifier];
	Person *person = [employees objectAtIndex:rowIndex];
	
	// Set the value for the attribute named identifier
	[person setValue:anObject forKey:identifier];
}

- (void)tableView:(NSTableView *)atableView
sortDescriptorsDidChange:(NSArray *)oldDescriptors
{
	NSArray *newDescriptors = [atableView sortDescriptors];
	[employees sortUsingDescriptors:newDescriptors];
	[tableView reloadData];
}

@end
Advertisements

Written by Clay

December 17, 2009 at 20:52

Posted in Cocoa, Code, Objective-C

Tagged with , , ,

3 Responses

Subscribe to comments with RSS.

  1. Your code gives a warning like “receiver is a forward class and corresponding @interface may not exist”. It still runs, but it isn’t completely clean.

    Just add #import “Person.h” at the beginning of MyDocument.m.

    Thanks for posting this!

    john

    March 15, 2010 at 19:52

  2. Thank you very much, I was stuck on that for some times…in no where the book mentioned sort key had to be set in IB.

    Someone

    March 21, 2010 at 21:26

  3. Yeah I was confused over this for a long time, I think I deserve some sleep!

    1111Charels

    June 13, 2011 at 17:59


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: