Cameron Hotchkies

Categories

  • Coding

Tags

  • NSTableView
  • PlanuKit
  • PlanuPlanu
  • cocoa
  • cocoa-bindings
  • objective-c

Lately, I’ve been brushing up on my Cocoa by writing a Lion client for Planets Nu called PlanuPlanu. I’ve opened up the core of this as an LGPL library called PlanuKit. It’s nice to work on projects with no pending deadlines to learn facets of a technology you don’t have time to learn during business hours.

This past weekend I was playing with NSTableView and Cocoa Bindings. By playing with, I mean I actually got off my ass and learned how they work and how to use them. Coming from an iOS background, where bindings don’t really exist, I’d been previously wiring all my NSTableViews together with datasource implementations. That is a perfectly acceptable way to do it, but using bindings is so much faster I’ll admit it was a dumb thing to put off learning.

For a space strategy game, like Planets Nu, there are inevitably a lot of tables worth generating. For example, the one I was working on with bindings was a list of all of the active player’s ships aka the Fleet Manifest: Screenshot of the fleet manifest

The contents of this NSTableView are simply assigned from an NSArray passed in as a property to the WindowController. The NSArray is just an array of NuShip objects (see https://bitbucket.org/HandsomeCam/planukit/src/ for the data structure, it’s large so I won’t be posting that here).

This is where I seem to differ with what everyone else appears to be doing. I have classes already and I’m not about to go converting them all to NSDictionaries just to match the rest of the world. There is no hard requirement that UI elements need to be bound to an NSDictionary, it’s trivial to do without using an NSDictionary at all.

Simply add an NSArrayController to your xib file. To take advantage of the automatic sorting, turn on “Auto Rearrange Content” on the NSArrayController, but don’t touch it’s bindings in any way.

For each of the Table Columns:

  • In the Value Binding section turn on “Bind To: Array Controller
  • Leave the Controller Key set to arrangedObjects
  • Set the Model Key Path to the name of your property

Adding the Model Key Path

  • In the attributes inspector, set the Sort Key to the name of your property
  • The selector will default to compare: which is what you want 99% of the time

Adding the Sort Key and Selector

If the property is an NSInteger, it will implicitly convert to an NSValue and display as a string fine.

Now for certain bindings, you may want to massage the data a bit. Scroll back up to the Fleet Manifest and look at the Beams or Torpedos columns. The string displayed has the following logic:

Beam weapon display decisions

The logic combining two pieces of data or none makes standard binding format strings unusable. If it’s just binding directly to a view, you can add a property to your view/window controller that reads the data from the source and bind to that property. For NSTableView columns you have different options.

  1. Don’t bind. Implement those columns with datasource implementations.
  2. Modify the underlying class to have the display format that only you want and nobody else really cares about.
  3. Get fancy.

For #1: I have a simple reaction. Fuck that noise. The whole reason we’re using bindings is to not write datasource implementations.

For #2: this may or may not actually be a viable option. In my case it easily could be as I am the only developer/consumer of both PlanuKit and PlanuPlanu, but the whole reason I broke the framework out was to give people the useful parts without cluttering it up. If you’re implementing against someone else’s library you might not even have this option.

So this leaves us with #3: Let us be getting of the fancy variety, which is the whole point of personal projects… right? This is where categories come into play. If you’ve never used a category in Objective-C before, you’re missing one of the coolest features of the language. Nothing is immutable, the universe is yours! Categories let you add functionality to existing objects without the need to subclass them. You can’t add properties per se, but a property is just syntactic sugar covering an underlying selector of the same name. In this instance, I created a category for NuShip called WeaponDisplay.

@implementation NuShip (WeaponDisplay) 
    - (NSString*)displayBeams { 
        if (self.beam == nil) { 
            return @"none"; 
        } 
    
        return [NSString stringWithFormat:@"%ld %@", 
            self.beams, 
            self.beam.name];
    } 
    
    - (NSString*)displayLaunchers { 
        if (self.launcher == nil) { 
            return @"none"; 
        } 
        
        return [NSString stringWithFormat:@"%ld %@", 
            self.torps, 
            self.launcher.name]; 
    } 
@end 

From here, you just #import the category’s header file into the header of the window/view controller where the NSTableView lives and you can add the display selector to the binding as if it’s a property of the object that has always been there.

Easy, clean, no datasource required. Now that’s how it’s done, son.