Search code examples
iosswiftswift3uicollectionviewuicollectionviewlayout

UICollectionView that does not scroll


I'm trying to create a UICollectionView with all cells 100% visible, such that I will not need scrolling to see them all. I'm currently trying to get a 3x3 grid displayed, and calculating the size of the cells on the fly.

I have the CollectionView and a UIView for a header in a Container View. The header is pinned to the top of the container with a height of 100px. The CollectionView is below that, pinned to each side, the bottom, and has its top pinned to the bottom of the header.

When I use sizeForItemAt, I'm trying to find the size of the visible area to split it up into 1/3 sized chunks (padding/insets aside). My code looks like:

func collectionView(_ collectionView: UICollectionView,
                    layout collectionViewLayout: UICollectionViewLayout,
                    sizeForItemAt indexPath: IndexPath) -> CGSize {

    let numRows = self.numRows()
    let itemsPerRow = self.itemsPerRow()

//        let frameSize = collectionView.frame.size
    let frameSize = collectionView.bounds.size
//        let frameSize = collectionView.collectionViewLayout.collectionViewContentSize
//        let frameSize = collectionView.intrinsicContentSize

    let totalItemPadding = self.itemPadding * (itemsPerRow - 1)
    let totalLinePadding = self.linePadding * (numRows - 1)

    let availableWidth = frameSize.width - totalItemPadding
    var widthPerItem = availableWidth / itemsPerRow

    let availableHeight = frameSize.height - totalLinePadding
    var heightPerItem = availableHeight / numRows

    return CGSize(width: widthPerItem, height: heightPerItem)
}

The result is always that the 3rd row is about half-obscured, as it looks like the frameSize is "taller" than it actually displays in the simulator.

The bottom row is chopped in half vertically

Is there something in UICollectionView that would give me the visible size? Am I at a wrong time in terms of layout timing, or should I add another method that invalidates size at some point?

I haven't found any tutorials out there for a collection view that shows all items, and does not vertically scroll, so any other pointers (or even libraries that do something like this) would be greatly appreciated.

Thanks!


Solution

  • Are you sure this method is getting called? Either add a log statement or a breakpoint in this routine and make sure it's getting called.

    A common problem that would prevent this from getting called would be if you neglected formally declare your view controller to conform to UICollectionViewDelegateFlowLayout. In that case, it would use whatever it found in the storyboard. But when I did this, your code worked fine for me, for example:

    extension ViewController: UICollectionViewDelegateFlowLayout {
    
        func collectionView(_ collectionView: UICollectionView,
                            layout collectionViewLayout: UICollectionViewLayout,
                            sizeForItemAt indexPath: IndexPath) -> CGSize {
            let numRows = self.numRows()
            let itemsPerRow = self.itemsPerRow()
    
            let frameSize = collectionView.bounds.size
    
            let layout = collectionViewLayout as! UICollectionViewFlowLayout
    
            let totalItemPadding = layout.minimumInteritemSpacing * (itemsPerRow - 1)
            let totalLinePadding = layout.minimumInteritemSpacing * (numRows - 1)
    
            let availableWidth = frameSize.width - totalItemPadding
            let widthPerItem = availableWidth / itemsPerRow
    
            let availableHeight = frameSize.height - totalLinePadding
            let heightPerItem = availableHeight / numRows
    
            return CGSize(width: widthPerItem, height: heightPerItem)
    
        }
    }
    

    Note, I also used minimumInteritemSpacing, so I use the existing spacing parameter rather than defining your own. It strikes me as better to use an existing parameter (esp one that you can also set in IB).


    By the way, the alternative, if it's always going to be on a single screen, is to use your own custom layout, rather than flow layout. That way you don't entangle the collection view's delegate with lots of cumbersome code. It would be a little more reusable. For example:

    class GridLayout: UICollectionViewLayout {
    
        var itemSpacing: CGFloat = 5
        var rowSpacing: CGFloat = 5
    
        private var itemSize: CGSize!
        private var numberOfRows: Int!
        private var numberOfColumns: Int!
    
        override func prepare() {
            super.prepare()
    
            let count = collectionView!.numberOfItems(inSection: 0)
    
            numberOfColumns = Int(ceil(sqrt(Double(count))))
            numberOfRows = Int(ceil(Double(count) / Double(numberOfColumns)))
    
            let width = (collectionView!.bounds.width - (itemSpacing * CGFloat(numberOfColumns - 1))) / CGFloat(numberOfColumns)
            let height = (collectionView!.bounds.height - (rowSpacing * CGFloat(numberOfRows - 1))) / CGFloat(numberOfRows)
            itemSize = CGSize(width: width, height: height)
        }
    
        override var collectionViewContentSize: CGSize {
            return collectionView!.bounds.size
        }
    
        override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
    
            attributes.center = centerForItem(at: indexPath)
            attributes.size = itemSize
    
            return attributes
        }
    
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            return (0 ..< collectionView!.numberOfItems(inSection: 0)).map { IndexPath(item: $0, section: 0) }
                .flatMap { layoutAttributesForItem(at: $0) }
        }
    
        private func centerForItem(at indexPath: IndexPath) -> CGPoint {
    
            let row = indexPath.item / numberOfColumns
            let col = indexPath.item - row * numberOfColumns
    
            return CGPoint(x: CGFloat(col) * (itemSize.width + itemSpacing) + itemSize.width / 2,
                           y: CGFloat(row) * (itemSize.height + rowSpacing) + itemSize.height / 2)
        }
    
        override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
            return true
        }
    }
    

    And then, in the view controller:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        let layout = GridLayout()
        layout.itemSpacing = 10
        layout.rowSpacing = 5
        collectionView?.collectionViewLayout = layout
    }