UICollectionView horizontal paging – can I use Flow Layout?

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:
Illustration of what I’m trying to do

  • Cannot add right bar button in navigation bar in storyboards iOS8
  • Customkeyboard Implemention with UISearchBar?
  • Apply gradient color to arc created with UIBezierPath
  • How does iOS app Fing get MAC Address?
  • How to add Label in BWWalkthrough View Controller with Swift 3?
  • Resizing an UITextView when the keyboard pops up with auto layout
  • 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?

    8 Solutions Collect From Internet About “UICollectionView horizontal paging – can I use Flow Layout?”

    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.

    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))
    }
    

    }

    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
        }
    }
    

    Can simply change Scroll Direction in UICollectionView.xib to Horizontal. And use with the correct order of elements in the array.

    This is the Swift 3 version of @GuilhermeSprint answer

    Code:

    public class HorizontalCollectionViewLayout : UICollectionViewLayout {
        var itemSize = CGSize(width: 0, height: 0) {
            didSet {
                invalidateLayout()
            }
        }
        private var cellCount = 0
        private var boundsSize = CGSize(width: 0, height: 0)
    
        public override func prepare() {
            cellCount = self.collectionView!.numberOfItems(inSection: 0)
            boundsSize = self.collectionView!.bounds.size
        }
        public override var 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
        }
    
        public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            var allAttributes = [UICollectionViewLayoutAttributes]()
            for i in 0...(cellCount-1) {
                let indexPath = IndexPath(row: i, section: 0)
                let attr = self.computeLayoutAttributesForCellAt(indexPath: indexPath)
                allAttributes.append(attr)
            }
            return allAttributes
        }
    
        public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
            return computeLayoutAttributesForCellAt(indexPath: indexPath)
        }
    
        public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
            return true
        }
    
        private func computeLayoutAttributesForCellAt(indexPath:IndexPath)
            -> 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(forCellWith: indexPath)
    
                var frame = CGRectMake(0, 0, 0, 0)
                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
        }
    }
    

    Usage:

        // I want to have 4 items in the page / see screenshot below
        let itemWidth = collectionView.frame.width / 2.0
        let itemHeight = collectionView.frame.height / 2.0
        let horizontalCV = HorizontalCollectionViewLayout();
        horizontalCV.itemSize = CGSize(width: itemWidth, height: itemHeight)
        collectionView.collectionViewLayout = horizontalCV
    

    Result

    screenshot

    My Delegates extension if you wanna check it also

    extension MyViewController : UICollectionViewDelegateFlowLayout, UICollectionViewDataSource{
        // Delegate
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            print("Clicked")
        }
    
        // DataSource
        func numberOfSections(in collectionView: UICollectionView) -> Int {
            return 1
        }
    
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BottomMenuCCell.xib, for: indexPath) as? BottomMenuCCell {
                cell.ibi = bottomMenuButtons[indexPath.row]
                cell.layer.borderWidth = 0
                return cell
            }
            return BaseCollectionCell()
        }
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            return CGSize.init(width: (collectionView.width / 2.0), height: collectionView.height / 2.0)
        }
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return bottomMenuButtons.count
        }
    
        // removing spacing between items
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
            return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        }
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
            return 0.0
        }
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
            return 0.0
        }
    }
    

    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