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:
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
}
}
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.
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;
}
It's a basically bug in NSCollectionView. As a workaround you can implement the scrollDirection method (from NSCollectionViewFlowLayout) and return NSCollectionViewScrollDirectionHorizontal.
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