The most straightforward way is to subclass NSCollectionViewFlowLayout
. That layout is almost what you want - it will always have the same number of rows and items per row that you are looking for: you just want them to be centered.
The main idea is to take the frames that NSCollectionViewFlowLayout
comes up with for each item, subtract those widths from the total width, and then update the frames so that they are distributed evenly.
As an overview, these are the steps:
Override prepareLayout
to calculate the number of columns in the current layout and the whitespace needed between each element (and the edges). This is done here so that we only need to calculate the values once.
Override layoutAttributesForElementsInRect
. Here, get the NSCollectionViewLayoutAttributes
for each item in the given rect and adjust the x position of the origin based on the column the item is in the and grid spacing calculated above. Return the new attributes.
Override shouldInvalidateLayoutForBoundsChange
to always return YES
as we need to recalculate everything when the bounds change.
I have a working sample application that demonstrates this here:
https://github.com/demitri/CenteringCollectionViewFlowLayout
but this is the full implementation:
//
// CenteredFlowLayout.m
//
// Created by Demitri Muna on 4/10/19.
//
#import "CenteredFlowLayout.h"
#import <math.h>
@interface CenteredFlowLayout()
{
CGFloat itemWidth; // width of item; assuming all items have the same width
NSUInteger nColumns; // number of possible columns based on item width and section insets
CGFloat gridSpacing; // after even distribution, space between each item and edges (if row full)
NSUInteger itemCount;
}
- (NSUInteger)columnForIndexPath:(NSIndexPath*)indexPath;
@end
#pragma mark -
@implementation CenteredFlowLayout
- (void)prepareLayout
{
[super prepareLayout];
id<NSCollectionViewDelegateFlowLayout,NSCollectionViewDataSource> delegate = (id<NSCollectionViewDelegateFlowLayout,NSCollectionViewDataSource>)self.collectionView.delegate;
NSCollectionView *cv = self.collectionView;
if ([delegate collectionView:cv numberOfItemsInSection:0] == 0)
return;
itemCount = [delegate collectionView:cv numberOfItemsInSection:0];
// Determine the maximum number of items per row (i.e. number of columns)
//
// Get width of first item (assuming all are the same)
// Get the attributes returned by NSCollectionViewFlowLayout, not our method override.
NSUInteger indices[] = {0,0};
NSCollectionViewLayoutAttributes *attr = [super layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathWithIndexes:indices length:2]];
itemWidth = attr.size.width;
NSEdgeInsets insets;
if ([delegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)])
insets = [delegate collectionView:cv layout:self insetForSectionAtIndex:0];
else
insets = self.sectionInset;
// calculate the number of columns that can fit excluding minimumInteritemSpacing:
nColumns = floor((cv.frame.size.width - insets.left - insets.right) / itemWidth);
// is there enough space for minimumInteritemSpacing?
while ((cv.frame.size.width
- insets.left - insets.right
- (nColumns*itemWidth)
- (nColumns-1)*self.minimumInteritemSpacing) < 0) {
if (nColumns == 1)
break;
else
nColumns--;
}
if (nColumns > itemCount)
nColumns = itemCount; // account for a very wide window and few items
// Calculate grid spacing
// For a centered layout, all spacing (left inset, right inset, space between items) is equal
// unless a row has fewer items than columns (but they are still aligned with that grid).
//
CGFloat totalWhitespace = cv.bounds.size.width - (nColumns * itemWidth);
gridSpacing = floor(totalWhitespace/(nColumns+1)); // e.g.: | [x] [x] |
}
- (NSUInteger)columnForIndexPath:(NSIndexPath*)indexPath
{
// given an index path in a collection view, return which column in the grid the item appears
NSUInteger index = [indexPath indexAtPosition:1];
NSUInteger row = (NSUInteger)floor(index/nColumns);
return (index - (nColumns * row));
}
- (NSArray<__kindof NSCollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(NSRect)rect
{
// We do not need to modify the number of rows/columns that NSCollectionViewFlowLayout
// determines, we just need to adjust the x position to keep them evenly distributed horizontally.
if (nColumns == 0) // prepareLayout not yet called
return [super layoutAttributesForElementsInRect:rect];
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
if (attributes.count == 0)
return attributes;
for (NSCollectionViewLayoutAttributes *attr in attributes) {
NSUInteger col = [self columnForIndexPath:attr.indexPath]; // column number
NSRect newFrame = NSMakeRect(floor((col * itemWidth) + gridSpacing * (1 + col)),
attr.frame.origin.y,
attr.frame.size.width,
attr.frame.size.height);
attr.frame = newFrame;
}
return attributes;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(NSRect)newBounds
{
return YES;
}
@end