Stefano Mtangoo
Stefano Mtangoo

Reputation: 6560

UICollectionViewController make Cell cover full width

I have a working list with UICollectionViewController (but the issue might be relevant for bare UICollectionView, I have not tested). It is working like Image below, but the issue is, the width varies contents.

I want the width to fill the width and let the height calculate itself based on contents. So far I have not been able to put a coherent solution. I have tried to override FlowLayout but then I have to pass the height of the cell which I don't know at that moment.

What is the solution that iOS devs use to solve such a problem. It looks to me common sense thing but apple seems to have complicated this one for no reason.

enter image description here

Upvotes: 2

Views: 661

Answers (2)

Vlad
Vlad

Reputation: 6732

Swift 5 example of full width cells when standard UICollectionViewFlowLayout is used.

Idea behind:

  • We are informing UICollectionViewFlowLayout that we want to have self-sizing cells.
  • Then we passing current collection view width to each cell.
  • Cell overrides preferredLayoutAttributesFitting(...) method. Then it performs desired size calculation using passed width. Desired size calculated using systemLayoutSizeFitting(width: ...).
class MyVievControler: UICollectionViewController {

   enum CellId: String {
      case red, green, blue
   }

   let items = repeatElement([CellId.red, .green, .blue], count: 40).reduce(into: []) { $0 += $1 }

   init() {
      let layout = UICollectionViewFlowLayout()

      // We want self-sizing cells
      layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

      layout.minimumInteritemSpacing = 0
      layout.minimumLineSpacing = 0
      super.init(collectionViewLayout: layout)
   }

   required init?(coder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
   }

   override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
      return items.count
   }

   override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
      let cellId = items[indexPath.row]
      switch cellId {
      case .red:
         let cell = collectionView.dequeueReusableCell(MyCellRed.self, indexPath: indexPath)
         cell.contentWidth = collectionView.bounds.width
         cell.text = indexPath.item % 2 == 0 ? StubObject().text.x128 : StubObject().text.x32
         return cell
      case .green:
         let cell = collectionView.dequeueReusableCell(MyCellGreen.self, indexPath: indexPath)
         cell.contentWidth = collectionView.bounds.width
         return cell
      case .blue:
         let cell = collectionView.dequeueReusableCell(MyCellBlue.self, indexPath: indexPath)
         cell.contentWidth = collectionView.bounds.width
         return cell
      }
   }
}


private class MyCellRed: UICollectionViewCell {

   var contentWidth: CGFloat = 100
   private lazy var label = UILabel()
   var text: String? {
      didSet {
         label.text = text
      }
   }

   override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
      var newFrame = layoutAttributes.frame

      // For better performance this size need to be cached as long as Cell content/model is not changed.
      let desiredSize = systemLayoutSizeFitting(width: contentWidth, verticalFitting: .fittingSizeLevel)

      newFrame.size = CGSize(width: contentWidth, height: desiredSize.height)
      layoutAttributes.frame = newFrame
      log.debug("Red: Calculated frame: \(newFrame). Desired size: \(desiredSize)")
      return layoutAttributes
   }

   override init(frame: CGRect) {
      super.init(frame: frame)
      backgroundView = UIView(backgroundColor: .red)
      layer.setBorder(width: 0.5)

      contentView.addSubview(label)
      label.translatesAutoresizingMaskIntoConstraints = false
      anchor.pin.toBounds(insets: 8, label).activate()
      label.layer.borderWidth = 1
      label.numberOfLines = 0
      label.textColor = .white
   }

   required init?(coder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
   }
}

private class MyCellGreen: UICollectionViewCell {

   var contentWidth: CGFloat = 100

   override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
      var newFrame = layoutAttributes.frame
      newFrame.size = CGSize(width: contentWidth, height: 40)
      layoutAttributes.frame = newFrame
      log.debug("Green: Calculated frame: \(newFrame)")
      return layoutAttributes
   }

   override init(frame: CGRect) {
      super.init(frame: frame)
      backgroundView = UIView(backgroundColor: .green)
      layer.setBorder(width: 0.5)
   }

   required init?(coder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
   }
}

private class MyCellBlue: UICollectionViewCell {

   var contentWidth: CGFloat = 100

   override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
      var newFrame = layoutAttributes.frame
      newFrame.size = CGSize(width: contentWidth, height: 100)
      layoutAttributes.frame = newFrame
      log.debug("Blue: Calculated frame: \(newFrame)")
      return layoutAttributes
   }
   
   override init(frame: CGRect) {
      super.init(frame: frame)
      backgroundView = UIView(backgroundColor: .blue)
      layer.setBorder(width: 0.5)
   }

   required init?(coder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
   }
}

Result:

Result

Upvotes: 1

Stefano Mtangoo
Stefano Mtangoo

Reputation: 6560

After reading this Article (Thanks to @DonMag), I was able to achieve what I wanted. So basically you extend the Cell class, and do some changes to CollectionView and its flowLayout. Here are the classes

FullWidthCollectionViewCell: All your cells in need of full width should extend it instead of UICollectionViewCell

class FullWidthCollectionViewCell: UICollectionViewCell
{
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        var modifiedTargetSize = targetSize
        modifiedTargetSize.height = CGFloat.greatestFiniteMagnitude
        
        let size = super.systemLayoutSizeFitting(
            modifiedTargetSize,
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: .fittingSizeLevel
        )
        return size
    }
}

Add extension to UICollectionView class. I have single file for all my view extensions but you can put it in the same file as CollectionView code

extension UICollectionView {
    var widestCellWidth: CGFloat {
        let insets = contentInset.left + contentInset.right
        return bounds.width - insets
    }
}

Finally in your CollectionView code. In my case I was using UICollectionViewController so All is set for me. If you are using UICollectionView as view in some custom UIViewController then you must adjust the code accordingly

class MyCollectionViewController: UICollectionViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let layout = collectionView.collectionViewLayout
        if let flowLayout = layout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = CGSize(
                width: collectionView.widestCellWidth,
                // Make the height a reasonable estimate to
                // ensure the scroll bar remains smooth
                height: 200
            )
        }
    }
    
    //Delegate and other methods here
    
}

That is all to enjoy. Again thanks to @DonMag for the link!

Upvotes: 1

Related Questions