|
Leopard introduces a bunch of amazingly powerful new controls, but one of my favorite new controls is the NSCollectionView. This control works a lot like the FlowLayoutPanel if you're familiar with Windows Presentation Foundation (WPF). It essentially is a layout container responsible for laying out a collection of subviews. You can either manually create the subview collection, or you can set the content array of the NSCollectionView. This is a really powerful option because if you can set the content array, you can also bind it. For this demo, I've bound the content array of the NSCollectionView to an array controller. If you follow along (or if you cheat and just download the code), you'll notice that the NSCollectionView subviews automatically request Core Animation layers. This means that, by default, new items fade in as they are added, but you can change that transition using the animations tab of the inspector.
To get started, just create a new Cocoa application. The first thing you do after creating the new Cocoa application is edit the project settings and set Garbage Collection to Required. Why you ask? Well, as a .NET developer, I have taken a blood oath to never write unmanaged code. If I violate this blood oath, I'm fairly certain that it will rip a hole in the fabric of space-time from which none of us will escape. So yeah, make sure GC is Required. The Cocoa application we're building is called MonsterCompendium. Rather than doing some contrived Hello World app, this app forms the basis of an app that a pen-and-paper DM might use to keep track of monsters. Obviously it's ridiculously oversimplified, but you get the idea.
I'm going to assume that you've done some Cocoa programming before (mostly because I don't have enough space to write the tutorial in a single blog post... maybe I should do a screencast?). Create yourself a new class called AppController and give it a method called newMonster and then make sure that you've got a "plus" button on your main window that invokes newMonster. Create a new class called KHMonster (feel free to change the initials so they match your name... or keep my initials if you really like them...). The source code to KHMonster.h and KHMonster.m is listed below. Take special note of the new property syntax and the fact that I've not written any retain/release code:
#import <Cocoa/Cocoa.h>
@interface KHMonster : NSObject
{
NSString *trueName;
int armorClass;
int attackRating;
}
@property(copy, readwrite) NSString *trueName;
@property int armorClass;
@property int attackRating;
@end
And here's the implementation (.m) code:
#import "KHMonster.h"
@implementation KHMonster
@synthesize trueName;
@synthesize armorClass;
@synthesize attackRating;
@end
With that out of the way, you can drag a new NSCollectionView onto your main window. Once you do this, you should notice that you get some extra goodies in your MainMenu.nib tray: an instance called Collection View Item and an NSView that represents the template for a single item within the collection. When you double click the NSView, define an interface that will be bound to a single instance of KHMonster. To do that, you need to set the value of your text fields to be bound to the Collection View Item (this is a special instance that acts like a proxy) with the model root path of : representedObject.trueName. representedObject is a property that points to the object being represented by that particular instance of the collection view item and trueName is a property on the KHMonster class.
In case you're curious, here's the code to the newMonster: method on the AppController class:
#import "AppController.h"
#import "KHMonster.h"
@implementation AppController
- (IBAction)newMonster:(id)sender
{
KHMonster *newMonster = [[KHMonster alloc] init];
newMonster.trueName = [NSString stringWithString:@"New Monster"];
newMonster.attackRating = 12;
newMonster.armorClass = 30;
[monsterArray addObject:newMonster];
}
@end
Note that I've got an array controller that is serving up instances of the KHMonster class. My AppController instance has an IBOutlet called monsterArray that I've rigged up through Interface Builder to allow the app controller to refer to the array controller as in the preceding code.
Finally, after all the code is together and all the bindings are rigged up, I can run the application. In the screenshot below, you can see me editing a borderless bold text field that contains the monster's name (bound to representedObject.trueName) and a couple labels and some text fields with number formatters in them to allow them to be bound to integers properly.

And in the next screenshot you see the collection view after editing a couple more items. The reason this is important is to show you that I'm not editing the same thing, but that I am actually working with individual elements within an array.

If you're familiar with Cocoa, then you know that this sample is only a few minutes worth of work away from being able to run against a local Core Data data source instead of the manual array controller of KHMonster objects.
I highly recommend you download the full sample and then go through the blog post again to see how the sample was created. Once you get the hang of using the NSCollectionView, you'll start to wonder how you ever lived without it. The keys to remember are:
I've been trying something similiar to your example - using core data
entities instead of creating my own item classes. After setting up the
NSCollectionView, and utilising representedObject on the subview, I can
display views with data bound to attributes of the objects in my
nsarraycontroller (looking similiar to your example). Sweet. Now ...
what I'd really like, is to be able to use this collectionview as a kind of
selectable list ... so, upon selecting a listitem view in the
collectionview, the selection of the bound arraycontroller would change,
and my interface could respond appropriately.
Some other nice new features in Leopard: template images, and the fact that
there are some default ones. For instance, instead of naming your buttons
“+” and “-”, you could set their image to NSAddTemplate and
NSRemoveTemplate, respectively. Just for kicks, try setting the button
behaviour to Toggle (but don’t leave it that way for +/- buttons).
Sweet. I had completely forgotten about the template images. That's one
thing that I thought was missing when using Tiger was the ability to use
some stock images/looks without jumping through flaming hoops.
newMonster.trueName = ;
or
Thanks - my first Obj-C 2.0 project!
Yes, you should probably be using NSInteger if you plan on doing
cross-architecture coding.
Even though showing several Obj-C 2.0 concepts is probably better for a
post like this, it's worth mentioning that the same project could be made
using Core Data with zero code and with the ability to save/load and undo
changes.
Yes, you'll note that I said it could all be done using Core Data and not
writing any code. The point, for me anyway, was to do it the "hard" way
first, then go to Core Data afterward. I've found that going straight to
Core Data as a newbie makes me skip some of the important details of MVC
and how bindings work. So yes, it would be easier to do it no-code with
Core Data, but far less illustrative of some key concepts.
I haven't opened the project, but I presume it requires binding the content
of the collection view to the array controller's arrangedObjects. To keep
the view selection synched with the array controller's selection just bind
the collection view's selectionIndexes binding to the arraycontroller's
selectionIndexes property.
Why copy the trueName property? Since you're creating a new KHMonster for
each monster, I wouldn't think having a separate copy of the ivar would
matter. Or is there some other reason I'm missing here?
What is wrong with NSCollectionView and tables?
In response to Jon... Kevin used copy for trueName to ensure that the value
would be protected from accidentally being changed outside the KHMonster
class. The actual setter would use something like "trueName = ;" while the
getter would use something like "return ;".