Tolgay Toklar Tolgay Toklar - 1 year ago 212
Swift Question

Dynamic-height cell in collection view

I want to resize cells like this app:

enter image description here

As you can see, right cells are shorter than left. To achive same thing i used following code:

var testI=1
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var cellHeight=287
if testI % 2 == 0 {
testI += 1
return CGSizeMake(CGFloat(176), CGFloat(cellHeight))


enter image description here

I dont want these top spaces. How can I remove them?
My collection view attrs:

enter image description here

How can I achive that?

Answer Source

UICollectionViewFlowLayout expects cells in question to have the same height (or width depending on scroll direction) so that it can correctly align them horizontally or vertically.

Thus, you can't use that to get the above effect. You need to create your own layout class which will have similar properties as UICollectionViewFlowLayout such as minimumInteritemSpacing, sectionInset etc. Besides, the new subclass (which I will name UserCollectionViewLayout from now on) will be capable of generating all the layout attributes for each corresponding cell with a frame value having different height so that cells will be positioned just like in the image you've shared.

Before we begin, you should at least have a basic understanding about how to create custom layouts so I strongly recommend you to read this article for a fresh start.

First, we need to create our own UICollectionViewLayoutAttributes class so we can store the height of the cell there for later use.

class UserCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
  var height: CGFloat = 0.0

Layout classes sometimes copy UICollectionViewLayoutAttributes when needed, so we need to override several methods to be able to pass height to copied object when it happens:

override func copyWithZone(zone: NSZone) -> AnyObject {
  let copy = super.copyWithZone(zone) as! UserCollectionViewLayoutAttributes
  copy.height = height
  return copy

override func isEqual(object: AnyObject?) -> Bool {
  if let object = object as? UserCollectionViewLayoutAttributes {
    if height != object.height {
      return false
    return super.isEqual(object)
  return false

We are going to need a new protocol which will ask for delegate the height of each cell:

(Similar to UICollectionViewDelegateFlowLayout.sizeForItemAtIndexPath)

protocol UserCollectionViewLayoutDelegate: UICollectionViewDelegate {
  func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UserCollectionViewLayout, heightForItemAtIndexPath indexPath: NSIndexPath) -> CGFloat 

OK, everything we need on our way to defining the layout is there. So, let's start:

class UserCollectionViewLayout: UICollectionViewLayout {
  private var contentHeight: CGFloat = 0.0
  private var allAttributes = [UserCollectionViewLayoutAttributes]()

  weak var delegate: UserCollectionViewLayoutDelegate!

  var numberOfColumns: Int = 2

  private var numberOfItems: Int {
    return collectionView?.numberOfItemsInSection(0) ?? 0

  private var contentWidth: CGFloat {
    return CGRectGetWidth(collectionView?.bounds ?? CGRectZero)

  private var itemWidth: CGFloat {
    return round(contentWidth / CGFloat(numberOfColumns))

OK, let's explain the above properties piece by piece:

  • contentHeight: Content height of collection view so it will know how to/where to scroll. Same as defining scrollView.contentSize.height.
  • allAttributes: An array holding the attributes of each cell. It will be populated in prepareLayout() method.
  • numberOfColumns: A value indicating how many cells will be shown at each row. It will be 2 in your example as you want to show 2 photos in a row.

Let's continue with overriding some important methods:

override func collectionViewContentSize() -> CGSize {
  return CGSize(width: contentWidth, height: contentHeight)

override class func layoutAttributesClass() -> AnyClass {
  return UserCollectionViewLayoutAttributes.self

The crucial part is to implement prepareLayout correctly. In this method, we are going to populate all the layout attributes corresponding to each indexPath, and store them in allAttributes for later use.

The layout looks like a 2D matrix of cells. However, height of each cell might be different. Thus, you should keep in mind that frame.origin.y value might not be same for cells positioned in the same row:

override func prepareLayout() {
  if allAttributes.isEmpty {
    let xOffsets = (0..<numberOfColumns).map { index -> CGFloat in
      return CGFloat(index) * itemWidth
    var yOffsets = Array(count: numberOfColumns, repeatedValue: CGFloat(0))
    var column = 0

    for item in 0..<numberOfItems {
      let indexPath = NSIndexPath(forItem: item, inSection: 0)
      let attributes = UserCollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)

      let cellHeight = delegate.collectionView(collectionView!, layout: self, heightForItemAtIndexPath: indexPath)

      let frame = CGRect(x: xOffsets[column], y: yOffsets[column], width: itemWidth, height: cellHeight)

      attributes.frame = frame
      attributes.height = cellHeight


      yOffsets[column] = yOffsets[column] + cellHeight

      column = (column >= numberOfColumns - 1) ? 0 : column + 1
      contentHeight = max(contentHeight, CGRectGetMaxY(frame))

override func invalidateLayout() {
  contentHeight = 0

We need to implement 2 more methods to let the layout know which layout attribute it should use upon displaying a cell which are:

Let's give it a shot:

override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  return cachedAttributes.filter { attribute -> Bool in
    return CGRectIntersectsRect(rect, attribute.frame)

override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
  return cachedAttributes.filter { attribute -> Bool in
    return attribute.indexPath == indexPath

OK, that's all for the layout part. Right now, we have a layout class capable of displaying cells with different height values in a single row. Now, we need to figure out how to pass height value to actual cell object.

Luckily for us, it is really straightforward if you are organizing your cell content using AutoLayout. There is a method named applyLayoutAttributes in UICollectionViewCell which enables you to make last minute changes on cell's layout.

class UserCell: UICollectionViewCell {

  let userView = UIView(frame: CGRectZero)
  private var heightLayoutConstraint: NSLayoutConstraint!

  override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes) {
    let attributes = layoutAttributes as! UserCollectionViewLayoutAttributes
    heightLayoutConstraint.constant = attributes.height

  override init(frame: CGRect) {
    super.init(frame: frame)
    contentView.backgroundColor = UIColor.whiteColor()
    userView.translatesAutoresizingMaskIntoConstraints = false

      options: NSLayoutFormatOptions.AlignAllCenterY,
      metrics: nil,
      views: ["userView": userView])

      options: NSLayoutFormatOptions.AlignAllCenterX,
      metrics: nil,
      views: ["userView": userView])

    heightLayoutConstraint = NSLayoutConstraint(
      item: self, 
      attribute: NSLayoutAttribute.Height, 
      relatedBy: NSLayoutRelation.Equal, 
      toItem: nil, 
      attribute: NSLayoutAttribute.NotAnAttribute, 
      multiplier: 0.0, 
      constant: 0.0
    heightLayoutConstraint.priority = 500

Getting all of them together, here is the result:

enter image description here

I am sure you can add minimumInteritemSpacing and sectionInset properties just like in UICollectionViewFlowLayout class in order to add some padding between cells.

(Hint: you should tweak cell's frame in prepareLayout method accordingly.)

You can download the example project from here.