Question

I'm using an NSCollectionView to display various objects. The whole things works rather well, except for one annoying thing. I cannot figure out how to access the various controls on the view used to represent each object in the collection.

Here's the setup:

  • I have dragged an NSCollectionView into my view in IB.
  • I made a custom subclass of NSCollectionViewItem. Mapped my class in IB.
  • I made a custom subclass of NSBox to act as the view for each object in the collection. Also mapped this class in IB and connected it to the view property of my NSCollectionViewItem subclass.
  • I made all the bindings in IB to display the correct information for each object.

The view:

enter image description here

The resulting collection view:

enter image description here

Reasoning that that my subclass of NSCollectionViewItem is basically a controller for each view in the collection, I made referencing outlets of the various controls in the view in my controller subclass:

@interface SourceCollectionViewItem : NSCollectionViewItem

@property (weak) IBOutlet NSTextField *nameTextField;
@property (weak) IBOutlet NSTextField *typeTextField;
@property (weak) IBOutlet RSLabelView *labelView;
@property (weak) IBOutlet NSButton *viewButton;

@end

When I inspect any instance of SourceCollectionViewItem in the debugger, all the properties show up as nil despite the fact that I can actually see them on my screen and that everything is displayed as it should be.

My setup was inspired by Apple's sample app IconCollection.

I am obviously missing something. What?

EDIT: I found various posts hinting at a similar issue: CocoaBuilder.com and this question on SO.

EDIT: Just to be complete: this post deals with the subject as well and delivers a solution based on a combination of the options mentioned in the accepted answer.

Was it helpful?

Solution

Outlets are set during nib loading, and only the prototype item is loaded from nib and has its outlets assigned. All other ViewItems and their Views are cloned from the prototype, in that case outlets are just instance variables that are never initialized.

Here are the options I could come up with:

  • Override newItemForRepresentedObject: of collection view and reload nib instead of cloning the prototype. But this will probably hurt the performance greatly.
  • Override copyWithZone of collection view item and assign outlets manually using viewWithTag: to find them.
  • Give up and try to provide data via bindings only.

OTHER TIPS

I found that overriding NSCollectionViewItem's -setRepresentedObject: could also be a good choice, as it is called on the new Item when all IBOutlet seem to be ready. After the call to super you can do whatever is needed:

- (void)setRepresentedObject:(id)representedObject
{
    if (representedObject) {
        [super setRepresentedObject:representedObject];
        [self.anOutlet bind:@"property" toObject:self.representedObject withKeyPath:@"representeProperty" options:nil];
    }
}

I used this method to bind a custom property of an interface object. The check is there to avoid useless calls, when the representedObject is not yet ready. The project uses a separate xib for the ViewItem, as explained in the links in the original edits.

Great question. Like @hamstergene suggests, you can use copyWithZone, it will be much more efficient compared to newItemForRepresentedObject. However viewWithTag is not always an option, first, because not everything can be tagged (easily), and, second, using tag for this purpose is a little wrong. Here's a cool approach with performance in mind, in Swift.

import AppKit

class MyViewController: NSCollectionItemView
{

    // Here you are cloning the original item loaded from the storyboard, which has 
    // outlets available, but as you've seen the default implementation doesn't take
    // care of them. Each view has a unique identifiers, which you can use to find it
    // in sublayers. What's really cool about this, is that you don't need to assign
    // any tags or do anything else while having advantage of better performance using
    // cached nib object.

    override func copyWithZone(zone: NSZone) -> AnyObject {
        let copy: NSCollectionItemView = super.copyWithZone(zone) as! NSCollectionItemView
        let oldView: RecordingView = self.view as! MyView
        let newView: RecordingView = copy.view as! MyView

        newView.foo = newView.viewWithIdentifier(oldView.foo.identifier!) as! NSTextfield
        newView.bar = newView.viewWithIdentifier(oldView.bar.identifier!) as! NSImageView

        return copy
    }
}

@IBDesignable class MyView: View
{

    // Custom collection view item view. Lets assume inside of it you have two subviews which you want
    // to access in your code.

    @IBOutlet weak var foo: NSTextfield!
    @IBOutlet weak var bar: NSImageView!
}

extension NSView
{

    // Similar to viewWithTag, finds views with the given identifier.

    func viewWithIdentifier(identifier: String) -> NSView? {
        for subview in self.subviews {
            if subview.identifier == identifier {
                return subview
            } else if subview.subviews.count > 0, let subview: NSView = subview.viewWithIdentifier(identifier) {
                return subview
            }
        }
        return nil
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top