Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should the contentView.translatesAutoResizingMaskToConstraints of a UICollectionViewCell subclass be set to `false`?

TL;DR

When trying to size UICollectionViewCells via auto layout, you can easily get auto layout warnings with even a simple example.

Should we be setting contentView.translatesAutoResizingMaskToConstraints = false to get rid of them?


I'm trying to create a UICollectionView with self sizing auto layout cells.

In viewDidLoad:

let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = CGSize(width: 10, height: 10)
collectionView.collectionViewLayout = layout

My cell is very basic. It's a single blue view with width and height 75 for testing purposes. The constraints are created by pinning the view to the superview on all 4 edges, and giving it a height and width.

class MyCell: UICollectionViewCell {
  override init(frame: CGRect) {
    view = UIView()

    super.init(frame: frame)

    view.backgroundColor = UIColor.blueColor()
    contentView.addSubview(view)

    installConstraints()
  }

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

  var view: UIView

  func installConstraints() {
    view.translatesAutoresizingMaskIntoConstraints = false

    var c: NSLayoutConstraint

    // pin all edges
    c = NSLayoutConstraint(item: contentView, attribute: .Leading, relatedBy: .Equal, toItem: view, attribute: .Leading, multiplier: 1, constant: 0)
    c.active = true
    c = NSLayoutConstraint(item: contentView, attribute: .Trailing, relatedBy: .Equal, toItem: view, attribute: .Trailing, multiplier: 1, constant: 0)
    c.active = true
    c = NSLayoutConstraint(item: contentView, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: 0)
    c.active = true
    c = NSLayoutConstraint(item: contentView, attribute: .Bottom, relatedBy: .Equal, toItem: view, attribute: .Bottom, multiplier: 1, constant: 0)
    c.active = true

    // set width and height
    c = NSLayoutConstraint(item: view, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 75)
    c.active = true
    c = NSLayoutConstraint(item: view, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 75)
    c.active = true
  }
}

When running the code, I get two errors about not being able to simultaneously satisfy constraints:

Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
    (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSAutoresizingMaskLayoutConstraint:0x7fed88c1bac0 h=--& v=--& H:[UIView:0x7fed8a90c2f0(10)]>",
    "<NSLayoutConstraint:0x7fed8ab770b0 H:[UIView:0x7fed8a90bbe0(75)]>",
    "<NSLayoutConstraint:0x7fed8a90d610 UIView:0x7fed8a90c2f0.leading == UIView:0x7fed8a90bbe0.leading>",
    "<NSLayoutConstraint:0x7fed8ab005f0 H:[UIView:0x7fed8a90bbe0]-(0)-|   (Names: '|':UIView:0x7fed8a90c2f0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x7fed8ab770b0 H:[UIView:0x7fed8a90bbe0(75)]>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

(The second error about the vertical constraints is omitted but it's nearly identical.)

These errors make sense. Essentially, the contentView (0x7fed8a90c2f0) has translatesAutoresizingMask = true, thus its bounds set to 10x10 (from the estimated size) creates that NSAutoresizingMaskLayoutConstraint constraint. There's no way the view can be both 10 pixels wide and 75 pixels wide, so it outputs an error.

I tried several different things to resolve this problem, with only 2 of them seeming to work.

Solution 1: Set at least one of the constraints to 999 or less

In this case, if I change the constraint that pins the bottom of the blue view to the bottom of the contentView to 999 priority (and do the same with the trailing constraint), it resolves the problem. Similarly, I could have chosen Top+Leading, or Width+Height so long as I don't have 1000 priorities in every direction. At least one has to "give" for the initial layout.

Although it may sound like this would cause the view to have an incorrect height/width, it actually appears to be the correct size.

Solution 2: Set contentView.translatesAutoresizingMaskIntoConstraints = false

By setting this to false, it essentially gets rid of the NSAutoresizingMaskLayoutConstraint constraint, and we no longer have a conflict. On the other hand, since the contentView is not set up with auto layout to begin with, it's as though no matter how you change its frame, its superview (the cell) is never going to update its size because there is nothing "pushing" back against it.

Nevertheless, this solution somehow magically works. I assume that at some point the cell looks at the contentView's frame and decides "hey you set me to this size, that probably means you want the cell to be this size as well" and takes care of resizing the cell for you.

Which solution should we use?

Are there better solutions to this problem? Which of the above should be used? Are there any downsides to the two solutions mentioned above, or are they essentially the same and it doesn't matter which one you use?


Notes:

  • The reason it is harder to see in examples such as DGSelfSizingCollectionViewCells is because they are relying on intrinsicContentSize + contentCompressionResistancePriority. If you get rid of my width+height constraints, replace them with 1000 setContentCompressionResistancePriority calls (horizontal+vertical), and use something with an intrinsic size (e.g. a label with text), you will no longer see the auto layout errors. This implies that width@1000 behaves differently than an intrinsicWidth + contentCompressionResistance@1000. Perhaps the time a view gets its intrinsic width is after it's okay to modify the the frame of the content view.

Things that didn't work:

  • I noticed that when I create cells via a storyboard, I usually never run in to this problem. When I examined the differences in the view of a cell created via a storyboard and one created programmatically, I noticed that the autoresizingMask was RM+BM (36) in the storyboard, but 0 when created via code. I tried manually setting the autoresizingMask of my blue view to 36, but that didn't work. The real reason that the storyboard solution works is that usually the constraints you create are already perfectly set in the storyboard (otherwise you would have storyboard errors that need to be fixing). As part of fixing these errors, you may change the bounds of the cell. Since it calls init?(coder:), the bounds of the cell are already configured correctly. So even though the contentView.autoresizingMask = true, there are no conflicts since it's already defined at the correct size.
  • In the linked guide, it links to this post when talking about creating constraints. It mentions that constraints should be created in updateConstraints. However, it sounds like this was Apple's original recommendations and that they are no longer recommending this convention for most of your constraints. Nevertheless, since that article says to create them in updateConstraints I tried to no avail. I got the same results. This is because the frame wasn't updated by the time updateConstraints was called (it was still 10x10).
  • I've noticed similar problems when you choose a estimated size that is too small. I've fixed it in the past by increasing the estimated size. In this case I tried 100x100, but it still caused the same errors. This makes sense... a view can't be both 100 wide and 75 wide at the same time. (Note that I consider setting it to 75x75 a non-solution. See below for more info.)

Non-solutions:

  • Setting the estimated size to 75x75. Although this will resolve the errors for this simple example, it won't resolve the problem for cells that truly are dynamically sized. I want a solution that'll work everywhere.
  • Returning the exact dimensions of the cell in collectionView(_:layout:sizeForItemAtIndexPath:) (e.g. by creating a sizing cell). I want a solution that works with my cell directly without needing any bookkeeping or extra calculation.
like image 690
Senseful Avatar asked Jul 04 '16 23:07

Senseful


1 Answers

After testing things out, I noticed at least one reason to keep contentView.translatesAutoresizingMaskToConstraints = true.

If you use preferredLayoutAttributesFittingAttributes(_:) to alter the dimensions of a UICollectionViewCell, it works by sizing the contentView to the correct dimension. If you set contentView.translatesAutoresizingMaskToConstraints = false, you'll lose this functionality.

Therefore, I recommend using Solution 1 (altering at least one constraint in each dimension to be non-required). In fact, I created a wrapper for UICollectionViewCell that will handle both the required 999 constraint, and a way to get preferred height or width to function correctly.

By using this wrapper, you won't need to remember the intricacies of getting the contentView in a UICollectionViewCell to behave properly.


class CollectionViewCell<T where T: UIView>: UICollectionViewCell {

  override init(frame: CGRect) {
    preferredHeight = nil
    preferredWidth = nil

    super.init(frame: frame)
  }

  var preferredWidth: CGFloat?
  var preferredHeight: CGFloat?
  private(set) var view: T?

  func initializeView(view: T) {
    assert(self.view == nil)
    self.view = view

    contentView.addSubview(view)

    view.translatesAutoresizingMaskIntoConstraints = false

    var constraint: NSLayoutConstraint

    constraint = NSLayoutConstraint(item: view, attribute: .Top, relatedBy: .Equal, toItem: contentView, attribute: .Top, multiplier: 1, constant: 0)
    constraint.active = true
    constraint = NSLayoutConstraint(item: view, attribute: .Leading, relatedBy: .Equal, toItem: contentView, attribute: .Leading, multiplier: 1, constant: 0)
    constraint.active = true

    // Priority must be less than 1000 to prevent errors when installing
    // constraints in conjunction with the contentView's autoresizing constraints.
    let NonRequiredPriority: UILayoutPriority = UILayoutPriorityRequired - 1

    constraint = NSLayoutConstraint(item: view, attribute: .Bottom, relatedBy: .Equal, toItem: contentView, attribute: .Bottom, multiplier: 1, constant: 0)
    constraint.priority = NonRequiredPriority
    constraint.active = true
    constraint = NSLayoutConstraint(item: view, attribute: .Trailing, relatedBy: .Equal, toItem: contentView, attribute: .Trailing, multiplier: 1, constant: 0)
    constraint.priority = NonRequiredPriority
    constraint.active = true
  }

  override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    let newLayoutAttributes = super.preferredLayoutAttributesFittingAttributes(layoutAttributes)

    if let preferredHeight = preferredHeight {
      newLayoutAttributes.bounds.size.height = preferredHeight
    }
    if let preferredWidth = preferredWidth {
      newLayoutAttributes.bounds.size.width = preferredWidth
    }

    return newLayoutAttributes
  }
}

(Note: the init method is required due to a bug with generic subclasses of UICollectionViewCell.)

To register:

collectionView.registerClass(CollectionViewCell<UIView>.self, forCellWithReuseIdentifier: "Cell")

To use:

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
  let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! CollectionViewCell<UILabel>

  if cell.view == nil {
    cell.initializeView(UILabel())
  }

  cell.view!.text = "Content"
  cell.preferredHeight = collectionView.bounds.height

  return cell
}
like image 127
Senseful Avatar answered Sep 28 '22 09:09

Senseful