This is related to but distinct from To use Flow Layout, or to Customize?.
Here is an illustration of what I’m trying to do:
I’m wondering if I can do this with a UICollectionViewFlowLayout
, a subclass thereof, or if I need to create a completely custom layout? Based on the WWDC 2012 videos on UICollectionView, it appears that if you use Flow Layout with vertical scrolling, your layout lines are horizontal, and if you scroll horizontally, your layout lines are vertical. I want horizontal layout lines in a horizontally-scrolling collection view.
I also don’t have any inherent sections in my model - this is just a single set of items. I could group them into sections, but the collection view is resizable, so the number of items that can fit on a page would change sometimes, and it seems like the choice of which page each item goes on is better left to the layout than to the model if I don’t have any meaningful sections.
So, can I do this with Flow Layout, or do I need to create a custom layout?
Overview. A flow layout is a type of collection view layout. Items in the collection view flow from one row or column (depending on the scrolling direction) to the next, with each row containing as many cells as will fit. Cells can be the same sizes or different sizes.
You need to reduce the height of UICollectionView to its cell / item height and select " Horizontal " from the " Scroll Direction " as seen in the screenshot below. Then it will scroll horizontally depending on the numberOfItems you have returned in its datasource implementation.
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 dynamically private var cellHeight = 90 public 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)) }
}
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