Recently I’ve been teaching myself Cocoa. I’ve been following the excellent Cocoa Programming For Mac OS X by Aaron Hillegass. Quality reference material like this book and Apple’s documentation makes learning much easier. Apple’s material is consitent, consisce and in the most part complete. However, there is one class where Apple’s reference is quite poor; NSCollectionView.
NSCollectionView is similar to NSTableView; they both display data with the help of a prototype which is copied for each piece of data to be displayed. NSTableView uses the NSCell for the prototype. The NSCell draws its self directly onto the NSTableView. NSCollectionView uses NSCollectionViewItem for the prototype. NSCollectionViewItem is a simple controller (it inherits directly from NSObject), it has no visual element. NSCollectionViewItem has two properties, representedObject and view. representedObject holds the object that the view will display and is set by the NSCollectionView. It is the responsibility of the NSCollectionViewItem to provide the view object.
When an NSCollectionView is created in Interface Builder two additional objects are created:
- An NSCollectionViewItem which is connected to the prototype outlet of the NSCollectionView
- An NSView which is connected to the view outlet of the NSCollectionViewItem
The suggested method for constructing the view is to bind the controls to the representedObject of the NSCollectionViewItem. The bindings are copied for each new instance of NSView. Because of these bindings the view and the model are connected with no controller code. Great!
Well, it’s great for a short while. Problems arises when you want more than simple bindings between the view and the representedObject. There are two parts to this problem:
- where do we put our controller code?
- how do we access the IBOutlets specified in our controller code?
The NSCollectionViewItem is the controller that mediates between the item data and the item view thus making it the correct place to put this code. This is achieved by subclassing NSCollectionViewItem. Once NSCollectionViewItem has been subclassed we need to tell Interface Builder to use the subclass, this is done by changing the Class Identity in Interface Builder from NSCollectionViewItem to the NSCollectionViewItem subclass.
The next problem is that unlike the bindings the in the NSView, the IBOutlet’s specified in our NSCollectionViewItem subclass are not connected when the prototype is copied. So how do we connect the IBOutlet’s specified in our NSCollectionViewItem subclass to the controls in the view? This problem is trivial once you realise that Interface Builder is not being very clever.
Interface Builder puts the custom NSView in the same nib as the NSCollectionView and NSCollectionViewItem. This is dumb. The solution is to move the NSView to its own nib and get the controller to load the view programmatically:
- Move the NSView into its own nib (thus breaking the connection between the NSCollectionViewItem and NSView).
- In I.B., change the Class Identity of File Owner to the NSCollectionViewItem subclass.
- Connect the controls to the File Owner outlets.
- Finally get the NSCollectionViewItem subclass to load the nib:
/*This might not be the best place for LoadFromNib:. If it was place in setRepresentObject: we could load different views depending on the class of the representedObject.*/
id result = [super copyWithZone:zone];
[NSBundle LoadFromNib:@"viewItem" owner: result];
//we can configure other aspects of result too
[result setPopupMenuDelegate: [self popupMenuDelegate];
Problem solved. We now have much more control of NSCollectionView. (Remember you can still bind to representObject).