1

I'm currently working on an app where we need to show items in a grid, with a fixed number of columns. So a UICollectionView with a custom UICollectionViewFlowLayout is great for this!

I created the following custom UICollectionViewFlowLayout that calculates the item size depending on the collectionView/container width, the desired number of columns and the desired spacing between the items (left of the first item, between the items, and right of the last item). So if we want two columns, it is going to be like this:

spacing - item - spacing - item - spacing

The custom UICollectionViewFlowLayout

import UIKit

class MultiColumnCollectionViewFlowLayout: UICollectionViewFlowLayout {

  let columnCount: Int = 2
  let itemAspectRatio: CGFloat = 0.71
  let spacing: CGFloat = 8

  var containerWidth: CGFloat = .zero {
    didSet {
      if containerWidth != oldValue {
        self.invalidateLayout()
      }
    }
  }

  // MARK: - Init

  override init() {
    super.init()
    self.configure()
  }

  required init?(coder: NSCoder) {
    super.init(coder: coder)
    self.configure()
  }

  // MARK: - Layout

  override func invalidateLayout() {
    super.invalidateLayout()
    self.configure()
  }

  func configure() {
    self.scrollDirection = .vertical
    self.itemSize = UICollectionViewFlowLayout.automaticSize
    self.sectionInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
    self.minimumLineSpacing = spacing

    let numberOfSpacings = CGFloat(columnCount + 1)
    let itemWidth: CGFloat = (containerWidth-(spacing*numberOfSpacings))/CGFloat(columnCount)
    self.estimatedItemSize = CGSize(width: itemWidth, height: itemWidth*itemAspectRatio)
  }

  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    guard let att = super.layoutAttributesForElements(in: rect) else { return [UICollectionViewLayoutAttributes]() }
    var x: CGFloat = sectionInset.left
    var y: CGFloat = -1.0

    for a in att {
        if a.representedElementCategory != .cell { continue }

        if a.frame.origin.y >= y { x = sectionInset.left }
        a.frame.origin.x = x
        x += a.frame.width + minimumInteritemSpacing
        y = a.frame.maxY
    }
    return att
  }
}

Problem:

With 1 item it works as expected: enter image description here

With multiple items (let's say 3 items), it shows this (not expected): enter image description here

What I want to happen, is something like this: enter image description here

Fun fact: when I try this at an iPad with 3 columns, different spacing and different item aspect ratio, it works fine: enter image description here

I debugged it and for the scenario with 2 columns on an iPhone 8 (iOS 13.3) (snapshots are taken on this device), itemAspectRatio 0.71 and spacing 8 (so the width of the collection view is 375 on an iPhone 8), the itemWidth should be 175.5. It is exactly that item width when I debug it, if you calculate 175,5+175,5+8+8+8 = 375, (2 items + 3 times spacing), so that should fit perfectly. For some reason, it doesn't. Let's say it is driving me nuts. Please help.

Charlotte1993
  • 589
  • 4
  • 27

0 Answers0