Search code examples
iosswiftautolayout

UICollectionView - constraints warning when I reselect item


I have custom UICollectionView

class MyCollectionViewCell: UICollectionViewCell {

    var v0: UIImageView = {
        let iv = UIImageView()
        iv.image = UIImage()
        iv.contentMode = .scaleAspectFit
        iv.translatesAutoresizingMaskIntoConstraints = false
        return iv
    }()
    
    var v1: UILabel = {
        let lb = UILabel()
        lb.font = UIFont.systemFont(ofSize: 16, weight: .regular)
        lb.textAlignment = .center
        lb.backgroundColor = .clear
        lb.translatesAutoresizingMaskIntoConstraints = false
        return lb
    }()
    
    var v2: UILabel = {
        let lb = UILabel()        
        lb.font = UIFont.systemFont(ofSize: 16, weight: .regular)
        lb.textAlignment = .center
        lb.backgroundColor = .clear
        lb.clipsToBounds = true
        lb.translatesAutoresizingMaskIntoConstraints = false
        return lb
    }()
     
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addViews()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        addViews()
    }
            
    private func addViews() {
                        
        let stack = UIStackView(arrangedSubviews: [v0, v1, v2])
        stack.distribution = .equalSpacing
        stack.alignment = .fill
        stack.axis = .vertical
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        self.addSubview(stack)
        
        NSLayoutConstraint.activate([
                        
            v0.heightAnchor.constraint(equalToConstant: 24),
            v0.widthAnchor.constraint(equalTo: v0.heightAnchor, multiplier: 1.5),
            
            v1.widthAnchor.constraint(equalTo: v1.heightAnchor),
            v2.widthAnchor.constraint(equalTo: v2.heightAnchor),
            
            stack.topAnchor.constraint(equalTo: self.topAnchor, constant: 0),
            stack.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0),
            stack.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0),
            stack.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0)
        ])
    }
}

I use this inside standard UICollectionView with UICollectionViewFlowLayout.

let layout = HorizontalLayout()
layout.minimumInteritemSpacing = 2
layout.minimumLineSpacing = 2
layout.scrollDirection = .horizontal
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

When the view is first shown, there is no warning. However, when I manually select a cell using

colView.reloadData()
colView.layoutIfNeeded()
colView.selectItem(at: IndexPath(row: activeIndex, section: 0),
                   animated: false,
                   scrollPosition: .centeredHorizontally)

I got a lot of constraints warnings from the cell that complains almost on every constraint.

For example:

[LayoutConstraints] 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. 
(
    "<NSLayoutConstraint:0x2833edc20 UIImageView:0x12a06e580.height == 24   (active)>",
    "<NSLayoutConstraint:0x2833edc70 UIImageView:0x12a06e580.width == 1.5*UIImageView:0x12a06e580.height   (active)>",
    "<NSLayoutConstraint:0x2833eddb0 H:|-(0)-[UIStackView:0x12a06ed20]   (active, names: '|':app.MyCollectionViewCell:0x12a06e310 )>",
    "<NSLayoutConstraint:0x2833ede00 UIStackView:0x12a06ed20.trailing == app.MyCollectionViewCell:0x12a06e310.trailing   (active)>",
    "<NSLayoutConstraint:0x2833ee670 'UISV-canvas-connection' UIStackView:0x12a06ed20.leading == UIImageView:0x12a06e580.leading   (active)>",
    "<NSLayoutConstraint:0x2833ee490 'UISV-canvas-connection' H:[UIImageView:0x12a06e580]-(0)-|   (active, names: '|':UIStackView:0x12a06ed20 )>",
    "<NSLayoutConstraint:0x2833d4b90 'UIView-Encapsulated-Layout-Width' app.MyCollectionViewCell:0x12a06e310.width == 50   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x2833edc20 UIImageView:0x12a06e580.height == 24   (active)>

What is wrong and why the problem appears only after manual selection?


Solution

  • First, when using a UICollectionViewCell (and when using a UITableViewCell), all subviews should be added to the cell's contentView:

    //self.addSubview(stack)
    contentView.addSubview(stack)
    

    and:

    //stack.topAnchor.constraint(equalTo: self.topAnchor, constant: 0),
    //stack.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0),
    //stack.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0),
    //stack.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0)
    
    stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0),
    stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0),
    stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0),
    stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0)
    

    Next, to avoid the auto layout complaints in debug console, you want to give the Flow layout a reasonable .estimatedItemSize

    The layout you've shown gives you an image view at 36 x 24 plus two 36 x 36 labels in a vertical stack view... so we can use an accurate size:

    layout.estimatedItemSize = .init(width: 36.0, height: 96.0)
    

    Now you can call .selectItem(at: ...) without getting the error/warning messages.


    Edit - a little clarification...

    It's very common to use "auto-sizing" cells - probably more common than fixed-size cells.

    When scrolling through the collection view, auto-layout processes constraints on the cell's UI elements as the cells are created/reused. Generally, as long as the constraints are setup correctly, everything works as expected.

    However, when executing code that "jumps" to a cell, auto-layout uses the .estimatedItemSize for its initial framing calculations. We can think of it as a "hint" we give to auto-layout.

    UICollectionViewFlowLayout.automaticSize is equal to:

    CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
    

    or

    CGSize(width: 1.7976931348623157e+308, height: 1.7976931348623157e+308)
    

    In this specific case, where the cell's are all equal widths (and we know that width), we can specify an exact width for .estimatedItemSize

    layout.estimatedItemSize = .init(width: 36.0, height: 96.0)
    

    to avoid the complaints.

    UIKit does a LOT of work managing elements like collection views ... and it's not unusual to see these types of error/warning messages in the debug console. As long as you understand why they're being generated, and your code is producing the layout you want, you can safely ignore the warnings.