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.
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.
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.
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:
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:
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.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).Non-solutions:
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.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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With