UICollectionView horizontal paging - can I use Flow Layout?
Here I share my simple implementation!
The .h file:
/** * CollectionViewLayout for an horizontal flow type: * * | 0 1 | 6 7 | * | 2 3 | 8 9 | ----> etc... * | 4 5 | 10 11 | * * Only supports 1 section and no headers, footers or decorator views. */@interface HorizontalCollectionViewLayout : UICollectionViewLayout@property (nonatomic, assign) CGSize itemSize;@end
The .m file:
@implementation HorizontalCollectionViewLayout{ NSInteger _cellCount; CGSize _boundsSize;}- (void)prepareLayout{ // Get the number of cells and the bounds size _cellCount = [self.collectionView numberOfItemsInSection:0]; _boundsSize = self.collectionView.bounds.size;}- (CGSize)collectionViewContentSize{ // We should return the content size. Lets do some math: NSInteger verticalItemsCount = (NSInteger)floorf(_boundsSize.height / _itemSize.height); NSInteger horizontalItemsCount = (NSInteger)floorf(_boundsSize.width / _itemSize.width); NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount; NSInteger numberOfItems = _cellCount; NSInteger numberOfPages = (NSInteger)ceilf((CGFloat)numberOfItems / (CGFloat)itemsPerPage); CGSize size = _boundsSize; size.width = numberOfPages * _boundsSize.width; return size;}- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{ // This method requires to return the attributes of those cells that intsersect with the given rect. // In this implementation we just return all the attributes. // In a better implementation we could compute only those attributes that intersect with the given rect. NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:_cellCount]; for (NSUInteger i=0; i<_cellCount; ++i) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0]; UICollectionViewLayoutAttributes *attr = [self _layoutForAttributesForCellAtIndexPath:indexPath]; [allAttributes addObject:attr]; } return allAttributes;}- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{ return [self _layoutForAttributesForCellAtIndexPath:indexPath];}- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{ // We should do some math here, but we are lazy. return YES;}- (UICollectionViewLayoutAttributes*)_layoutForAttributesForCellAtIndexPath:(NSIndexPath*)indexPath{ // Here we have the magic of the layout. NSInteger row = indexPath.row; CGRect bounds = self.collectionView.bounds; CGSize itemSize = self.itemSize; // Get some info: NSInteger verticalItemsCount = (NSInteger)floorf(bounds.size.height / itemSize.height); NSInteger horizontalItemsCount = (NSInteger)floorf(bounds.size.width / itemSize.width); NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount; // Compute the column & row position, as well as the page of the cell. NSInteger columnPosition = row%horizontalItemsCount; NSInteger rowPosition = (row/horizontalItemsCount)%verticalItemsCount; NSInteger itemPage = floorf(row/itemsPerPage); // Creating an empty attribute UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; CGRect frame = CGRectZero; // And finally, we assign the positions of the cells frame.origin.x = itemPage * bounds.size.width + columnPosition * itemSize.width; frame.origin.y = rowPosition * itemSize.height; frame.size = _itemSize; attr.frame = frame; return attr;}#pragma mark Properties- (void)setItemSize:(CGSize)itemSize{ _itemSize = itemSize; [self invalidateLayout];}@end
And finally, if you want a paginated behaviour, you just need to configure your UICollectionView:
_collectionView.pagingEnabled = YES;
Hoping to be useful enough.
Converted vilanovi code to Swift in case someone, needs it in the future.
public class HorizontalCollectionViewLayout : UICollectionViewLayout {private var cellWidth = 90 // Don't kow how to get cell size dynamicallyprivate var cellHeight = 90public override func prepareLayout() {}public override func collectionViewContentSize() -> CGSize { let numberOfPages = Int(ceilf(Float(cellCount) / Float(cellsPerPage))) let width = numberOfPages * Int(boundsWidth) return CGSize(width: CGFloat(width), height: boundsHeight)}public override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? { var allAttributes = [UICollectionViewLayoutAttributes]() for (var i = 0; i < cellCount; ++i) { let indexPath = NSIndexPath(forRow: i, inSection: 0) let attr = createLayoutAttributesForCellAtIndexPath(indexPath) allAttributes.append(attr) } return allAttributes}public override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! { return createLayoutAttributesForCellAtIndexPath(indexPath)}public override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { return true}private func createLayoutAttributesForCellAtIndexPath(indexPath:NSIndexPath) -> UICollectionViewLayoutAttributes { let layoutAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath) layoutAttributes.frame = createCellAttributeFrame(indexPath.row) return layoutAttributes}private var boundsWidth:CGFloat { return self.collectionView!.bounds.size.width}private var boundsHeight:CGFloat { return self.collectionView!.bounds.size.height}private var cellCount:Int { return self.collectionView!.numberOfItemsInSection(0)}private var verticalCellCount:Int { return Int(floorf(Float(boundsHeight) / Float(cellHeight)))}private var horizontalCellCount:Int { return Int(floorf(Float(boundsWidth) / Float(cellWidth)))}private var cellsPerPage:Int { return verticalCellCount * horizontalCellCount}private func createCellAttributeFrame(row:Int) -> CGRect { let frameSize = CGSize(width:cellWidth, height: cellHeight ) let frameX = calculateCellFrameHorizontalPosition(row) let frameY = calculateCellFrameVerticalPosition(row) return CGRectMake(frameX, frameY, frameSize.width, frameSize.height)}private func calculateCellFrameHorizontalPosition(row:Int) -> CGFloat { let columnPosition = row % horizontalCellCount let cellPage = Int(floorf(Float(row) / Float(cellsPerPage))) return CGFloat(cellPage * Int(boundsWidth) + columnPosition * Int(cellWidth))}private func calculateCellFrameVerticalPosition(row:Int) -> CGFloat { let rowPosition = (row / horizontalCellCount) % verticalCellCount return CGFloat(rowPosition * Int(cellHeight))}
}
You're right – that's not how a stock horizontally-scrolling collection view lays out cells. I'm afraid that you're going to have to implement your own custom UICollectionViewLayout
subclass. Either that, or separate your models into sections.