1

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!

Hoopes
  • 3,943
  • 4
  • 44
  • 60

4 Answers4

1

For the layout size, you could use UICollectionViewFlowLayout and relate the dimensions in terms of your screen width and height so as to preserve the grid style look of the UICollectionView. See: https://developer.apple.com/documentation/uikit/uicollectionviewflowlayout and https://www.raywenderlich.com/136159/uicollectionview-tutorial-getting-started.

As for your scrolling issue, check that your scrolling is enabled, your constraints are setup correctly, and you don't have the problem mentioned at this link. UICollectionView does not scroll

Good luck! :)

Mihir Thanekar
  • 508
  • 4
  • 8
  • Hi, I'm sorry I wasn't more clear. I don't actually _want_ the view to scroll - I want to get all of the stuff on the screen (in full size), so I don't have to scroll. I had originally done this with nested stack views, but the flexibility of the collection view seems attractive (I'm like two weeks into ios programming, fwiw) – Hoopes Aug 06 '17 at 23:34
1

Your scrolling is not working because the view is just the size of the screen...

Just to get the trick add an offset of 1000. The amount of the total height of the collectionView is every row * (row.size.height + padding).

Johannes Knust
  • 891
  • 1
  • 11
  • 18
  • Hi John, thanks for the response. I turned scrolling off - I want to see ALL of the cells in the collection view without scrolling. In the screenshot, you can see the bottom row being cut off, I'm trying to find out why (I spent all day on trying to figure out why `collectionView.frame` height was not correct - I'm pretty sure my math is. – Hoopes Aug 06 '17 at 23:35
  • I‘m not able in the next hours to check but you should print your vales to get the mistake, I agree math seems fine. What I would do first is using self.view and from the the sizes ... I will check later – Johannes Knust Aug 07 '17 at 06:05
1

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
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Dude. DUDE. The grid layout worked first try - did you write that, or did you find it somewhere? If you wrote it, I owe you a beer, if you found it, could you point me to where you did? Exactly what I needed, thanks so much. – Hoopes Aug 06 '17 at 23:40
  • Also, the code was indeed getting called, I had all the classes set up right, I just couldn't figure out why `collectionView.bounds.size` and/or `collectionView.frame.size` were not giving me the correct height. – Hoopes Aug 06 '17 at 23:41
  • What did you mean by `if it's always going to be on a single screen`? I wanted to implement paging with arrow buttons at some point, am I painting myself into a corner with the grid layout? – Hoopes Aug 06 '17 at 23:47
  • Yeah, I wrote that, but I wouldn’t be surprised if others wrote something similar. Re why your initial attempt didn’t work, I’m not sure, but at least you now have a solution that works. I think the custom layout is the best way to tackle it, anyway. – Rob Aug 06 '17 at 23:48
  • If you want to later horizontally scroll (for example when the number per page exceeds some value), you would have to alter the custom layout. It’s not rocket science, but will take a little work. Or use a horizontal flow layout with paging turned on. – Rob Aug 06 '17 at 23:52
  • @Hoopes - Actually, making it multipage was pretty simple. See https://github.com/robertmryan/GridLayoutDemo. – Rob Aug 07 '17 at 05:02
0

In my case this was a lot simpler, the basic idea was to add extra padding to the end of collection view. On my case my orientation was horizontal but the same should apply to a vertical one (have not tested that one) The trick was to adjust the delegate function as follow:

  extension ViewController: UICollectionViewDelegateFlowLayout {

        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    
            return UIEdgeInsets.init(top: collectionView.bounds.width * 0.02, left: collectionView.bounds.width * 0.02, bottom: collectionView.bounds.width * 0.02, right: collectionView.bounds.width * 0.22)
      }
  }

Noticed the difference on the right value, in your case it would be the bottom value of the row.

Fidel
  • 1,173
  • 11
  • 21