Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSCollectionView custom layout enable scrolling

I can't get scrolling both vertically and horizontally to work with a custom layout for NSCollectionView. According to the docs, in my subclass I return the collectionViewContentSize and if that is too big, scrolling is automatically enabled in the enclosing scroll view of the collection view. However, even if I order all elements in a horizontal row, only vertical scrolling is enabled.

Here is a screenshot: Screenshot

Here is my layout code:

class Layout: NSCollectionViewLayout
{
var cellSize = CGSize(width: 100, height: 30)

var cellSpacing: CGFloat = 10
var sectionSpacing: CGFloat = 20

private var contentSize = CGSize.zero
private var layoutAttributes = [NSIndexPath: NSCollectionViewLayoutAttributes]()


override func prepareLayout() {
    guard let collectionView = collectionView else { return }

    let sections = collectionView.numberOfSections
    guard sections > 0 else { return }

    contentSize.height = cellSize.height

    for section in 0..<sections {
        let items = collectionView.numberOfItemsInSection(section)
        guard items > 0 else { break }

        for item in 0..<items {
            let origin = CGPoint(x: contentSize.width, y: 0)
            let indexPath = NSIndexPath(forItem: item, inSection: section)
            let attributes = NSCollectionViewLayoutAttributes(forItemWithIndexPath: indexPath)
            attributes.frame = CGRect(origin: origin, size: cellSize)
            layoutAttributes[indexPath] = attributes

            contentSize.width += cellSize.width + cellSpacing
        }
        contentSize.width += sectionSpacing
    }
}

override var collectionViewContentSize: NSSize {
    return contentSize
}

override func layoutAttributesForElementsInRect(rect: NSRect) -> [NSCollectionViewLayoutAttributes] {

    return layoutAttributes.values.filter { $0.frame.intersects(rect) }
}

override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> NSCollectionViewLayoutAttributes? {
    return layoutAttributes[indexPath]
}

override func shouldInvalidateLayoutForBoundsChange(newBounds: NSRect) -> Bool {
    return false
}
}
like image 219
DasNilpferd Avatar asked Jun 24 '16 14:06

DasNilpferd


3 Answers

This bug is still ongoing. I've converted Bo Yuan's nasty hack to Swift. (No offence, Bo Yuan, it's not YOUR fault we have to do this horrible workaround! I'm still ripping this code out as soon as there's an official fix though. For the time being this is just going to keep my development efforts going.)

class HackedCollectionView: NSCollectionView {
    override func setFrameSize(_ newSize: NSSize) {
        let size = collectionViewLayout?.collectionViewContentSize ?? newSize
        super.setFrameSize(size)
        if let scrollView = enclosingScrollView {
            scrollView.hasHorizontalScroller = size.width > scrollView.frame.width
        }
    }
}

Note that this definitely needs to go ASAP because it's getting called for every single frame of animation when scrolling. Not nice. Please, please, please someone either fix this issue or find a proper solution. Not being able to scroll horizontally is ridiculous.

like image 165
Ash Avatar answered Oct 21 '22 08:10

Ash


It seems that only NSCollectionViewFlowLayout is able to dictate a frame that has a width larger than the parent scroll views frame.

The solution is to subclass NSCollectionViewFlowLayout instead of NSCollectionViewLayout. Treat the subclass like any other layout subclass, but add the critical scrollDirection in prepareLayout().

Here is minimum implementation for a layout that scrolls horizontal, and just sets all the items next to one another.

-(void) prepareLayout
{
    [super prepareLayout];
    self.scrollDirection = NSCollectionViewScrollDirectionHorizontal;
}

-(NSSize) collectionViewContentSize
{
    NSSize itemSize = self.itemSize;
    NSInteger num = [self.collectionView numberOfItemsInSection:0];
    NSSize contentSize = NSMakeSize((num * itemSize.width) + ((num+1) * self.minimumLineSpacing), NSHeight(self.collectionView.frame));
    return contentSize;
}

-(BOOL) shouldInvalidateLayoutForBoundsChange:(NSRect)newBounds { return YES; }

-(NSArray<__kindof NSCollectionViewLayoutAttributes *> *) layoutAttributesForElementsInRect:(NSRect)rect
{
    int numItems = [self.collectionView numberOfItemsInSection:0];
    NSMutableArray* attributes = [NSMutableArray arrayWithCapacity:numItems];
    for (int i=0; i<numItems; i++)
    {
        [attributes addObject:[self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]];
    }
    return attributes;
}

-(NSCollectionViewLayoutAttributes*) layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSSize itemSize = self.itemSize;
    NSRect fr = NSZeroRect;
    fr.size = itemSize;
    fr.origin.y = (NSHeight(self.collectionView.frame) - itemSize.height) / 2.0;
    fr.origin.x = (indexPath.item+1 * self.minimumLineSpacing) + (indexPath.item * itemSize.width);
    NSCollectionViewLayoutAttributes* attr = [NSCollectionViewLayoutAttributes layoutAttributesForItemWithIndexPath:indexPath];
    attr.frame = fr;
    return attr;
}
like image 26
Steffan Avatar answered Oct 21 '22 10:10

Steffan


It's a basically bug in NSCollectionView. As a workaround you can implement the scrollDirection method (from NSCollectionViewFlowLayout) and return NSCollectionViewScrollDirectionHorizontal.

like image 25
Peder Avatar answered Oct 21 '22 10:10

Peder