Monday, October 12, 2009

Ch.. Ch.. Ch.. Changes



I struggled a long time with a bindings problem today. (This struggling is very typical of Cocoa programming, and motivates my posts of very simple working examples---the struggle is kind of like going to a mental gym). Anyway, I stripped the example to its essentials, and now it seems to work. Tomorrow I must find out how to apply it to what I really want to do.

Anyway, the example has an NSTableView which displays a single column of values that are, ultimately, strings. While one can use an array of NSMutableDictionaries as the source of the string for each row (I believe the object must respond to setObject: forKey:), I have implemented this as a simple class. The reason is that I want to observe the changes, and an NSMutableDictionary won't let me do that.

So I have a class "TableEntry" with a single instance variable "theName" and nearly all of it is two lines: @property and @synthesize in the right places. Not any code at all beyond that. I got a warning of a method declaration conflict which is why I switched from the more typical "name" to "theName." You know how to do the @property stuff. I did implement one method---description, to make the report log a little more useful.

In the nib, I bind the Array Controller to the App Delegate with a Model Key Path of self.theArray---I'm pretty sure the self part is unnecessary, but it's what IB prompts, so I say OK. (We'll examine the App Delegate itself below). And the table column is bound to the Array Controller's arrangedObjects with a Model Key Path of "theName."

What is happening is that the App Delegate's variable theArray contains an array of TableEntry objects, each of which has an appropriate variable with a string value, the variable is "theName." The code to set up the App Delegate and point it to the ultimate data source (objects of class "TableEntry") is this:

// interface

#import <Cocoa/Cocoa.h>

@interface TVBindings3AppDelegate : NSObject {
NSWindow *window;
}

@property (assign) IBOutlet NSWindow *window;
@property (retain) NSMutableArray *theArray;

- (IBAction)report:(id)sender;
- (IBAction)modify:(id)sender;

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;

@end

-------
// implementation

#import "TVBindings3AppDelegate.h"
#import "TableEntry.h"

@implementation TVBindings3AppDelegate

@synthesize window;
@synthesize theArray;

- (void)awakeFromNib {
NSMutableArray *temp = [NSMutableArray
arrayWithCapacity:5];
TableEntry *e;
for (NSString *s in [NSArray
arrayWithObjects:@"a",@"b",@"c",nil]) {
e = [[TableEntry alloc] init];
[e setTheName:s];
[e addObserver:self
forKeyPath:@"theName"
options:NSKeyValueObservingOptionNew
context:NULL];
[temp addObject:e];
}
[self setTheArray:temp];
}


The "addObserver..." stuff is what we'll getting to in a minute.

It works great. I can see the table view when the app runs, and have a method to print the underlying data source (not shown, it's standard). Modifying either the UI table view or the underlying data does what it should. I think it's important that I use standard accessor to do the programmatic set:

- (IBAction)modify:(id)sender{
TableEntry *obj = [theArray objectAtIndex:0];
if ([obj theName] == @"Rex") {
[obj setTheName:@"George"];
}
else {
[obj setTheName:@"Rex"];
}
NSLog(@"modify theArray %@", theArray);
}


If you've followed any of the other bindings posts, up to this point, there should be no problem. Now, we want to be notified when a change occurs. In my case, this is because I will have another table column which needs to be re-calculated when the "amount" changes in my checkbook application.

We need to register ourselves as an observer. This code is in the App Delegate listing above. This is why we need a class rather than an NSMutableDictionary to hold the values. The registration stuff doesn't work with NSMutableDictionary.

The other thing is we need is to implement this method in the App Delegate (and declare it in the header):

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)obj
change:(NSDictionary *)change
context:(void *)context {
NSLog(@"observe %@ %@ %@", keyPath, obj, change);
}


The "change" dictionary contains variables based on the option we passed in: options:NSKeyValueObservingOptionNew. Since the objectAtIndex:0 starts with a "theName" key of "a", it gets changed to "Rex" and this is what we get back.

When I do "modify", the Console prints (logging info stripped):

observe theName Rex {
kind = 1;
new = Rex;
}
modify theArray (
Rex,
b,
c
)


From the docs, I guess that the "kind = 1" means that the key NSKeyValueChangeKindKey has a value of 1, that is, the enumerated value NSKeyValueChangeSetting.

I think we're in business.