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:
Can simply change Scroll Direction in UICollectionView.xib to Horizontal. And use with the correct order of elements in the array.
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))
}
}
The previous above implementation was not complete, buggy, and with fixed cell size. Here's a more literal translation for the code:
import UIKit
class HorizontalFlowLayout: UICollectionViewLayout {
var itemSize = CGSizeZero {
didSet {
invalidateLayout()
}
}
private var cellCount = 0
private var boundsSize = CGSizeZero
override func prepareLayout() {
cellCount = self.collectionView!.numberOfItemsInSection(0)
boundsSize = self.collectionView!.bounds.size
}
override func collectionViewContentSize() -> CGSize {
let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))
let itemsPerPage = verticalItemsCount * horizontalItemsCount
let numberOfItems = cellCount
let numberOfPages = Int(ceil(Double(numberOfItems) / Double(itemsPerPage)))
var size = boundsSize
size.width = CGFloat(numberOfPages) * boundsSize.width
return size
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var allAttributes = [UICollectionViewLayoutAttributes]()
for var i = 0; i < cellCount; i++ {
let indexPath = NSIndexPath(forRow: i, inSection: 0)
let attr = self.computeLayoutAttributesForCellAtIndexPath(indexPath)
allAttributes.append(attr)
}
return allAttributes
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return self.computeLayoutAttributesForCellAtIndexPath(indexPath)
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
func computeLayoutAttributesForCellAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes {
let row = indexPath.row
let bounds = self.collectionView!.bounds
let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))
let itemsPerPage = verticalItemsCount * horizontalItemsCount
let columnPosition = row % horizontalItemsCount
let rowPosition = (row/horizontalItemsCount)%verticalItemsCount
let itemPage = Int(floor(Double(row)/Double(itemsPerPage)))
let attr = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
var frame = CGRectZero
frame.origin.x = CGFloat(itemPage) * bounds.size.width + CGFloat(columnPosition) * itemSize.width
frame.origin.y = CGFloat(rowPosition) * itemSize.height
frame.size = itemSize
attr.frame = frame
return attr
}
}
Your last resort, of course, would be to use multiple vertically collection views inside each section in an outer horizontally scrolling collection view. Apart from increasing code complexity and difficulty in performing inter-section cell animations, I can't think of major issues with this approach right off my head.
@interface HorizontalCollectionViewLayout : UICollectionViewFlowLayout
@end
@implementation HorizontalCollectionViewLayout
- (instancetype)init
{
self = [super init];
if (self) {
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.minimumLineSpacing = 0;
self.minimumInteritemSpacing = 0;
}
return self;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *attributesArray = [super layoutAttributesForElementsInRect:rect];
NSInteger verticalItemsCount = (NSInteger)floorf(self.collectionView.bounds.size.height / self.itemSize.height);
NSInteger horizontalItemsCount = (NSInteger)floorf(self.collectionView.bounds.size.width / self.itemSize.width);
NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount;
for (NSInteger i = 0; i < attributesArray.count; i++) {
UICollectionViewLayoutAttributes *attributes = attributesArray[i];
NSInteger currentPage = (NSInteger)floor((double)i / (double)itemsPerPage);
NSInteger currentRow = (NSInteger)floor((double)(i - currentPage * itemsPerPage) / (double)horizontalItemsCount);
NSInteger currentColumn = i % horizontalItemsCount;
CGRect frame = attributes.frame;
frame.origin.x = self.itemSize.width * currentColumn + currentPage * self.collectionView.bounds.size.width;
frame.origin.y = self.itemSize.height * currentRow;
attributes.frame = frame;
}
return attributesArray;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
@end
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.