AppKit Release Notes for OS X v10.11 suggests that collection view items can be resized on a per-item basis:
Item size can be determined globally for all of a CollectionView’s items (by setting an NSCollectionViewFlowLayout’s “itemSize” property), or can be varied from one item to the next (by implementing -collectionView:layout:sizeForItemAtIndexPath: on your CollectionView’s delegate).
In my case, my CollectionViewItem consists of a single label that contains a string of varying length. I'm using the NSCollectionView to display an array of strings, as NSStackViews don't support array bindings, and don't flow to new lines. The array of strings is bound to the NSCollectionView's content via an array controller.
My item's nib file is properly set up, the root view and the label both have Content Hugging and Content Compression Resistance Priorities of 1000, and the edges are aligned via AutoLayout.
Now, the NSCollectionViewLayout's delegate method has this signature:
func collectionView(collectionView: NSCollectionView,
layout collectionViewLayout: NSCollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> NSSize
My idea now is to grab the item itself, run a layout pass over it, and then returning the new item size.
let item = collectionView.itemAtIndexPath(indexPath)!
item.view.layout()
return item.view.bounds.size
Problem with this approach is that itemAtIndexPath returns nil. If I return a default size in the nil case, that default size is used for all cells.
How can I set a NSCollectionView to respect my item's AutoLayout constraints and for each cell, use the computed size dynamically?
There's a duplicate of this question that I answered but this is probably the one it should be directed to since it's older.
Juul's comment is correct- the items do not exist. sizeForItemAt
being called is the collection asking the delegate for any specific sizing for that data entry with which it will use to help create its eventual view controller, NSCollectionViewItem
. So you create a loop when you ask the collection to get you an item in the method that it uses to help get an item.
The problem we have is that we want sizing based on the appearance of that data: the length of a text label with proper formatting, not just, say, the string length. So we hit a chicken and egg problem.
The only solution I've come to, which could be prettier, is the following:
NSCollectionViewItem
and ensure your collection view has a data source that returns the proper subclassed item.sizeForItemAt
call, or at the beginning of the first one if you hadn't by then, manually create an instance of your NSCollectionViewItem
subclass, and use NSNib's instantiate(withOwner:topLevelObjects:)
to instantiate its XIB with your subclass as an owner. Store that reference as a sort of "sizing template," so you only need to do it once. Delegate was easiest spot for me. ^Note: my first route was to attempt this through the collection's makeItemWithIdentifier
, but it was more brittle as it required the collection to have items at the time of creating the sizing template. It also could not be done during an initial sizeForItemAt
(accessing/making items during a reload crashes). And I was worried that because it was made with the collection it may get reused down the line and the methods below don't work or start editing visible items. YMMV.
sizeForItemAt
View.FittingSize
, the smallest size an item can be given its constraints/priorities, and return that. Bam! Hasn't been stress tested or anything but no problems on my end, and its not doing a layout pass or anything, just calling FittingSize
. I haven't seen this articulated anywhere online yet, so I wanted to write out the full explanation.
I did this in Xamarin.Mac, so my code won't be 1:1 and I don't want to write garbled swift and mess anything up.
TLDR: manually instantiate a NSCollectionViewItem subclass and its xib that you will store, unowned by the collection. During sizeForItem populate that item you store as a sizing reference, and return the FittingSize
of the collection item's view.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With