8

By default (i.e., with a vertical scrolling direction), the UICollectionViewFlowLayout lays out cells by starting at the top-left, going from left to right, until the row is filled, and then proceeds to the next line down. Instead, I would like it to start at the bottom-left, go from left to right, until the row is filled, and then proceed to the next line up.

Is there a straightforward way to do this by subclassing UIScrollViewFlowLayout, or do I basically need to re-implement that class from scratch?

Apple's documentation on subclassing flow layout suggests that I only need to override and re-implement my own version of layoutAttributesForElementsInRect:, layoutAttributesForItemAtIndexPath:, and collectionViewContentSize. But this does not seem straightforward. Since UICollectionViewFlowLayout does not expose any of the grid layout calculations it makes internally in prepareLayout, I need to deduce all the layout values needed for the bottom-to-top layout from the values it generates for a top-to-bottom layout.

I am not sure this is possible. While I can re-use its calculations about which groups of items get put on the same rows, I will need to calculate new y offsets. And to make my calculations I will need information about all the items, but those superclass methods do not report that.

Robin Daugherty
  • 7,115
  • 4
  • 45
  • 59
algal
  • 27,584
  • 13
  • 78
  • 80
  • Have you thought of transforming the whole collection view. using some transform might give you this effect. – Sandeep May 17 '14 at 20:04
  • I thought of applying a vertical reflection to whole view, but (a) I'd also need to apply a inverse transform to each cell individually and (b) I can't see how this would handle the case of cells that outside of the visible content area when the user scrolls. – algal May 17 '14 at 20:15
  • " I need to deduce all the layout values needed for the bottom-to-top layout from the values it generates for a top-to-bottom layout." No, because you are just swapping positions. Suppose there are 5 rows. Then you want to shift everything in row 1 to row 5. Well, you know the y value for something in row 5 because `super` tells you. – matt May 17 '14 at 20:23
  • @insane-36 I guess the problem is that I don't understand what we want to do or why. If the cells are in the wrong order, why not provide them in the right order (in the data source methods): why fight the flow layout by flowing backwards? To say that cell 0 appears at the bottom makes no sense to me, or if it does, then that is not a flow layout; a complete custom layout would be needed. But to say that the contents corresponding to item 0 in some array appear at the bottom is quite reasonable. – matt May 18 '14 at 02:54
  • @matt I'm doing this for a UI where collection items need to "pile up" on top on top of another element, so yes it's a custom layout not just a re-ordering of the datasource. "No, because you are just swapping positions" That's what I hoped! But I believe swapping y-values only works if the normal flow layout's collection view content size exactly fits the collection view's bounds. Because otherwise, using your example, row 5's y-value will likely be offset somewhat from the y-value that would be chosen for genuinely starting layout with a bottom row. – algal May 19 '14 at 06:40
  • @algal Well, I think in the end you're probably going to write your own layout from scratch, because that's the only way (as insane-36 has brilliantly explained) to get the data source to be asked for additional items as you scroll _up_. It will be interesting to see what you come up with! – matt May 19 '14 at 15:27

3 Answers3

4

The very helpful answer by @insane-36 showed a way to do it when collectionView.bounds == collectionView.collectionViewContentSize.

But if you wish to support the case where collectionView.bounds < collectionViewcontentSize, then I believe you need to re-map the rects exactly to support scrolling properly. If you wish to support the case where collectionView.bounds > collectionViewContentSize, then you need to override collectionViewContentSize to ensure the content rect is positioned at the bottom of the collectionView (since otherwise it will be positioned at the top, due to the top-to-bottom default behavior of UIScrollView).

So the full solution is a bit more involved, and I ended up developing it here: https://github.com/algal/ALGReversedFlowLayout.

algal
  • 27,584
  • 13
  • 78
  • 80
3

You could basically implement it with a simple logic, however this seems to be some how odd. If the collectionview contentsize is same as that of the collectionview bounds or if all the cells are visible then you could implement this with simple flowLayout as this,

@implementation SimpleFlowLayout


- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
  UICollectionViewLayoutAttributes *attribute = [super layoutAttributesForItemAtIndexPath:indexPath];
  [self modifyLayoutAttribute:attribute];
  return attribute;
}

- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect{
  NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
  for(UICollectionViewLayoutAttributes *attribute in attributes){
    [self modifyLayoutAttribute:attribute];
  }
  return attributes;
}


- (void)modifyLayoutAttribute:(UICollectionViewLayoutAttributes*)attribute{
  CGSize contentSize = self.collectionViewContentSize;
  CGRect frame = attribute.frame;
  frame.origin.x = contentSize.width - attribute.frame.origin.x - attribute.frame.size.width;
  frame.origin.y = contentSize.height - attribute.frame.origin.y - attribute.frame.size.height;
  attribute.frame = frame;

}

@end

And so the figure looks like this,

enter image description here

But, if you use more rows, more than the that can be seen on the screen at the same time, then there seems to be some problem with reusing. Since the UICollectionView datasource method, collectionView:cellForItemAtIndexPath: works linearly and asks for the indexPath as the user scrolls, the cell are asked in the usual increasing indexPath pattern such as 1 --- 100 though we would want it to reverse this pattern. While scrolling we would need the collectionView to ask us for the items in decreasing order since our 100 item resides at top and 1 item at bottom. So, I dont have any particular idea about how this could be accomplished.

Sandeep
  • 20,908
  • 7
  • 66
  • 106
  • Very nice discussion. – matt May 18 '14 at 02:53
  • @insane-36 Thanks for the taking the time! But I fear this works only when collectionViewContentSize == collectionView.bounds. If collectionViewContentSize < collectionView.bounds, it reverses the row ordering but does not offset the rows so that they start from the bottom. E.g., when there's just one row: this still puts it high up. And as you say, if collectionViewContentSize > collectionView.bounds, then it fails with scrolling. Also, the layoutAttributesForElementsInRect: will always give wrong results if passed a rect that only captures one element, no? Feels like this should be easier. – algal May 19 '14 at 06:59
2

UICollectionView with a reversed flow layout.

import Foundation
import UIKit

class InvertedFlowLayout: UICollectionViewFlowLayout {

    override func prepare() {
        super.prepare()
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard super.layoutAttributesForElements(in: rect) != nil else { return nil }
        var attributesArrayNew = [UICollectionViewLayoutAttributes]()

        if let collectionView = self.collectionView {
            for section in 0 ..< collectionView.numberOfSections {
                for item in 0 ..< collectionView.numberOfItems(inSection: section) {
                    let indexPathCurrent = IndexPath(item: item, section: section)
                    if let attributeCell = layoutAttributesForItem(at: indexPathCurrent) {
                        if attributeCell.frame.intersects(rect) {
                            attributesArrayNew.append(attributeCell)
                        }
                    }
                }
            }

            for section in 0 ..< collectionView.numberOfSections {
                let indexPathCurrent = IndexPath(item: 0, section: section)
                if let attributeKind = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: indexPathCurrent) {
                    attributesArrayNew.append(attributeKind)
                }
            }
        }

        return attributesArrayNew
    }

    override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributeKind = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath)

        if let collectionView = self.collectionView {
            var fullHeight: CGFloat = 0.0

            for section in 0 ..< indexPath.section + 1 {
                for item in 0 ..< collectionView.numberOfItems(inSection: section) {
                    let indexPathCurrent = IndexPath(item: item, section: section)
                    fullHeight += cellHeight(indexPathCurrent) + minimumLineSpacing
                }
            }

            attributeKind.frame = CGRect(x: 0, y: collectionViewContentSize.height - fullHeight - CGFloat(indexPath.section + 1) * headerHeight(indexPath.section) - sectionInset.bottom + minimumLineSpacing/2, width: collectionViewContentSize.width, height: headerHeight(indexPath.section))
        }

        return attributeKind
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributeCell = UICollectionViewLayoutAttributes(forCellWith: indexPath)

        if let collectionView = self.collectionView {
            var fullHeight: CGFloat = 0.0

            for section in 0 ..< indexPath.section + 1 {
                for item in 0 ..< collectionView.numberOfItems(inSection: section) {
                    let indexPathCurrent = IndexPath(item: item, section: section)
                    fullHeight += cellHeight(indexPathCurrent) + minimumLineSpacing

                    if section == indexPath.section && item == indexPath.item {
                        break
                    }
                }
            }

            attributeCell.frame = CGRect(x: 0, y: collectionViewContentSize.height - fullHeight + minimumLineSpacing - CGFloat(indexPath.section) * headerHeight(indexPath.section) - sectionInset.bottom, width: collectionViewContentSize.width, height: cellHeight(indexPath) )
        }

        return attributeCell
    }

    override var collectionViewContentSize: CGSize {
        get {
            var height: CGFloat = 0.0
            var bounds = CGRect.zero

            if let collectionView = self.collectionView {
                for section in 0 ..< collectionView.numberOfSections {
                    for item in 0 ..< collectionView.numberOfItems(inSection: section) {
                        let indexPathCurrent = IndexPath(item: item, section: section)
                        height += cellHeight(indexPathCurrent) + minimumLineSpacing
                    }
                }

                height += sectionInset.bottom + CGFloat(collectionView.numberOfSections) * headerHeight(0)
                bounds = collectionView.bounds
            }

            return CGSize(width: bounds.width, height: max(height, bounds.height))
        }
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        if let oldBounds = self.collectionView?.bounds,
            oldBounds.width != newBounds.width || oldBounds.height != newBounds.height {
            return true
        }

        return false
    }

    func cellHeight(_ indexPath: IndexPath) -> CGFloat {
        if let collectionView = self.collectionView, let delegateFlowLayout = collectionView.delegate as? UICollectionViewDelegateFlowLayout {
            let size = delegateFlowLayout.collectionView!(collectionView, layout: self, sizeForItemAt: indexPath)
            return size.height
        }

        return 0
    }

    func headerHeight(_ section: Int) -> CGFloat {
        if let collectionView = self.collectionView, let delegateFlowLayout = collectionView.delegate as? UICollectionViewDelegateFlowLayout {
            let size = delegateFlowLayout.collectionView!(collectionView, layout: self, referenceSizeForHeaderInSection: section)
            return size.height
        }

        return 0
    }
}
Karen Hovhannisyan
  • 1,140
  • 2
  • 21
  • 31