NSCollectionViewLayout horizontal and vertical scrolling

Hi,


I need to implement a custom NSCollectionViewLayout with support for both horizontal and vertical scrolling, if needed. The WWDC 2015 video and the doc say, that this case is handled by returning the appropriate content size. But I can't get horizontal scrolling to work.

I made a simple test layout that just puts items horizontally next to each other, so that `collectionViewContentSize` returns a width bigger than the screen of my MacBook. Still, the enclosing scrollview only enables vertical scrolling (and `hasHorizontalScroller` is set to yes). I'm using Xcode 7.3 with Swift 2.2, but the issue is also present in Xcode 8 beta 1 with Swift 3.


Here is the code of my layout class:

import Cocoa
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
    }
}

I'm not a specialist of CollectionViews.


But collectionViewContentSize returns the total width of your collection (right ?)

Then, no need to scroll, because the whole content should fit horizontally.


Test with


    override var collectionViewContentSize: NSSize {
        let clipBounds = self.collectionView?.superview?.bounds ?? NSRect()
        return clipBounds.size
    }

Well, the content does actually not fit horizontally.

This is a screenshot of how it looks like:

As you can see, the required width is much greater than the width of the scroll view. Actually there are 10 sections with 20 items each.

we do not get the images in the forum.


When I say fit, I mean collectionViewContentSize is large enough to fit everything.


Did you try with the other override func collectionViewContentSize?


Or even just return 200 as width, just to see.

I did notice 🙂

Here's a link: http:/ /i.stack.imgur.com/tMbCN.png

And yes, collectionViewContentSize is large enough to fit everything.

I also tried your code, and it looks exactly the same.

But now that clips the width of my layout to the superview's size?

May be you could try to override the collectionViewContentSize() method and not the property


override func collectionViewContentSize() -> CGSize {
return CGSize(width: contentSize.width, height: contentSize.height)
}


Not sure that will make a difference, but…

I tried it, it's not possible, the compiler won't compile saying the method cannot be named the same as a property on the same class. I believe in Swift `collectionViewContentSize` is a computed property, so I need to override that. I tried a ObjC version of the layout, that also did not work.


I also made a new project, added only a collection view, deleted the prototye segue from the storyboard, made a 'empty' datasource and a custom layout with its only contents being

override var collectionViewContentSize: NSSize {
        return NSSize(width: 3000, height: 3000)
}


In the `viewDidLayout` method of the view controller, I print the frame size of the scroll view's document view:

collectionView.enclosingScrollView?.documentView?.frame.size

This returns the correct height (3000), but not the correct width (it shows the width of the scroll view)


Edit: I also tried this on iOS with UICollectionView and the same layout and there it worked.

I have examples where I override the method without problem.


When you override the method, do you cancel the overriade of the property ?

I'm seeing the same issue with my custom NSCollectionViewLayout. In `viewDidLoad` I reload my collection view and print out the scroll view's `contentSize` vs. the collection view's `collectionViewContentSize`. I know that at least my collection view is reporting the correct content size:


NSLog(@"Scroll view size: %@", NSStringFromSize(self.scrollView.contentSize));
NSLog(@"Collection view content size: %@", NSStringFromSize(self.collectionView.collectionViewLayout.collectionViewContentSize));


which prints:


2016-07-07 13:48:19.968 MSOSXCollectionViewCalendarLayout[82908:19833103] Scroll view size: {716, 497}

2016-07-07 13:48:19.969 MSOSXCollectionViewCalendarLayout[82908:19833103] Collection view content size: {19486, 2100}


My scroll view correctly scrolls vertically, but no matter what I change I can't get it to scroll horizontally too. I've done the simple things like ensuring `setHasHorizontalScroller` is set to YES (obj-c). From the NSLog, the content size and collection view content size are what I'd expect. However, I can't scroll horizontally.

I tested this via a symbolic breakpoint on setHasHorizontalScroller and setHasVerticalScroller on NSScrollView. It appears these methods are being called multiple times, and one of these calls enables the vertical scroller and disables the horizontal scroller. I tried subclassing NSScrollView and always returning YES from these methods. This didn't work, and various combinations of hard coding a return value resulted in no vertical scrolling and very broken almost horizontal scrolling.


At this point I'm going to look for a solution not involving NSCollectionView.

I met the exact same issue with a custom NSCollectionViewLayout. Don't know how to fix it. I hope someone can give me some hint or maybe it is a bug from Apple's own sdk.

I think it's a bug and I've filed a bug report for it (rdar://27585965). I'd encourage you all to do so too in the hope that this gets pushed up the list and fixed.

I believe it has some relationship with auto layout. I could not figure out any clue yet.

I figured out a solution. I created a subclass of NSCollectionView. And use this subclass to replace the default NSCollectionView. I created function to override the setFrameSize. Voila!


And in this function:



-(void)setFrameSize:(NSSize)newSize{

if (newSize.width != self.collectionViewLayout.collectionViewContentSize.width) {

newSize.width = self.collectionViewLayout.collectionViewContentSize.width;

}

[super setFrameSize:newSize];

//NSLog(@"setFrameSize%@", CGSizeCreateDictionaryRepresentation(newSize));

}

@end


During my test, the setFrameSize will be invoked and some mechanism set the frame size to {0,0} first then set it again to make the width the same width as the clipview and keep the height from the layout's content size.


I hope it can give other developers some help. It really bothered me for a very looooong time. Now I have a custom NSCollectionViewLayout which can scroll horizontally and vertically!


Happy coding!

That helped me - thanks a lot! Filed a report too: rdar://29752068.


This may not apply to everyone, but for my needs I could have also went by with an NSCollectionViewGridLayout instead, restricting it to a single row, and making the min/maximum item sizes the same. However there was still a case when I made the collection view narrow in width where it caused multiple rows to show, rather than keeping the single row which is what I wanted - so in the end, I decided to go with subclassing NSCollectionViewLayout.

I hate to lift this out of the 7 year cold storage—but has there been any sort of progress on this issue?

Also seeing layouts that can't scroll horizontally, and applying the patch suggested (replacing the width in the setFrameSize of Collection View) hasn't turned out to be the most stable (occasionally, I am seeing a layout loop that eventually crashes the app).

NSCollectionViewLayout horizontal and vertical scrolling
 
 
Q