Search code examples
iosautolayoutuikituicollectionviewcellsizing

UICollectionViewCell: AutoLayout - incorrect size calculation (offset by exacatly 10 pt)


I'm having a set of UICollectionViewCell subclasses with different layout (using AutoLayout).

Since "Self Sizing Cells" feature is completely broken, I calculate size manually in the DataSource. However, I've noticed that the calculated size is exactly 10 pt less than it should be, regardless of the cell.

Here is the code I use to calculate size:

  let sizeCache = CellCache() // Size cache stores previously calculated values
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    if let cachedSize = sizeCache.sizeAtIndexPath(indexPath: indexPath) {
      return cachedSize // Return cached value if it exists
    }

    // Calculate the size if not found in the cache
    let cellClass = cellTypeAt(indexPath: indexPath) // Get the class type
    let cell = sizeCache.createCellOfTypeIfNeeded(cellType: cellClass) // Dequeue or instantiate a cell
    self.collectionView(collectionView, cell: cell, configureAt: indexPath) // Ask DataSource to configure this cell
    let fittingSize = CGSize(width: collectionView.bounds.width - 16, height: 0) // Set the padding to 16
    cell.bounds = CGRect(origin: .zero, size: fittingSize) // Set cell's width
    var result = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) // Calculate cell's size using AutoLayout
    result.width = collectionView.bounds.width - 16 // Set the padding
    result.height = result.height + 10 // !!! Error - Add 10 pt to the result !!!
    sizeCache.setSize(size: result, at: indexPath) // Cache the result
    return result
  }

Note the line at the end where I have to add 10 pt to make it work.

All my cells are a subclasses of this base class which sets the width constraint:

import UIKit
import SnapKit

class AutoSizingCellBase: UICollectionViewCell {
  override class var requiresConstraintBasedLayout: Bool {
    return true
  }

  private final var widthConstraint: Constraint?

  override func updateConstraints() {
    if widthConstraint == nil {
      if let window = window {
        let width = window.bounds.width - 16
        contentView.snp.makeConstraints { (make) in
          widthConstraint = make.width.equalTo(width).constraint
        }
      }
    }
    super.updateConstraints()
  }
}

The examples of the issue:

Correct sizing (after adding 10 pt) Correct sizing (after adding 10 pt

Incorrect sizing (without adding 10 pt) Incorrect sizing (without adding 10 pt)

The issue affects all the cells. What could be the root cause of it?

Update: Constraints Example Here are the constraints I use to configure the views shown:

  private func setupConstraints() {
    price.setContentCompressionResistancePriority(.required, for: .horizontal)
    disclosureIndicator.setContentCompressionResistancePriority(.required, for: .horizontal)
    disclosureIndicator.setContentCompressionResistancePriority(.required, for: .vertical)
    disclosureIndicator.setContentHuggingPriority(.required, for: .horizontal)
    disclosureIndicator.setContentHuggingPriority(.required, for: .vertical)

    title.snp.makeConstraints { (make) in
      make.leading.top.equalTo(contentView.layoutMarginsGuide)
      make.trailing.lessThanOrEqualTo(price.snp.leading).offset(-8)
    }

    subtitle.snp.makeConstraints { (make) in
      make.top.equalTo(title.snp.bottom).offset(8)
      make.leading.bottom.equalTo(contentView.layoutMarginsGuide)
    }

    disclosureIndicator.snp.makeConstraints { (make) in
      make.trailing.equalTo(contentView.layoutMarginsGuide)
      make.centerY.equalToSuperview()
    }

    price.snp.makeConstraints { (make) in
      make.trailing.equalTo(disclosureIndicator.snp.leading).offset(-8)
      make.centerY.equalToSuperview()
    }
  }

This is a rather complex example, but the issue is reproducible even with a more simpler ones:

  private func setupConstraints() {
    button.snp.makeConstraints { (make) in
      make.width.equalToSuperview() // Button is a subview of the UIStackView
    }

    stack.snp.makeConstraints { (make) in
      make.edges.equalTo(contentView.layoutMarginsGuide)
      make.height.greaterThanOrEqualTo(150)
    }
  }

Update

Adding the offset in the constraint solves the issue, while the view hierarchy debugger still shows "Ambiguous layout":

subtitle.snp.makeConstraints { (make) in
  make.top.equalTo(title.snp.bottom).offset(8)
  make.leading.equalTo(contentView.layoutMarginsGuide)
  make.bottom.equalTo(contentView.layoutMarginsGuide).offset(-10) // Subtracted 10
}

The question is where does the 10 comes from is still open.

Update 3 It seems that a part of the problem is in adjusting the layoutMargins:

UIView.appearance().layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)

After I've removed the line above from the AppDelegate, the size calculations became correct, although the looks of the cells changed. So I'm thinking that the problem is for some reason the cells created for sizing have a different inset than the ones dequeued from the UICollectionView.

Cells without insets


Solution

  • LayoutMargins are calculated differently for a UICollectionViewCell when added to a superview and when not in a view hierarchy.

    Because of the line:

    UIView.appearance().layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
    

    Cells added to the superview had different margins than the cells created for sizing. Hence the difference.