Swift: Constraint conflict with custom UICollectionViewListCell in modern CollectionView

Viewed 290

I am working with "modern" UICollectionView with iOS14 to implement a List configuration. My implementation is successful, ie cells are rendered with dynamic height sizing, but with conflicting constraints.

2020-12-24 19:23:50.889822+0800 circulars[18490:2636547] [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:0x60000102b250 V:|-(16)-[UIStackView:0x7f9b6ad4d120]   (active, names: '|':circulars.CircularCollectionCell:0x7f9b6ad4c5f0 )>",
    "<NSLayoutConstraint:0x60000102b4d0 UIStackView:0x7f9b6ad4d120.bottom == circulars.CircularCollectionCell:0x7f9b6ad4c5f0.bottom - 16   (active)>",
    "<NSLayoutConstraint:0x60000102b520 'UISV-canvas-connection' UIStackView:0x7f9b6ad4d120.top == UILabel:0x7f9b6ad49320.top   (active)>",
    "<NSLayoutConstraint:0x60000102b570 'UISV-canvas-connection' V:[UILabel:0x7f9b6ad4ceb0]-(0)-|   (active, names: '|':UIStackView:0x7f9b6ad4d120 )>",
    "<NSLayoutConstraint:0x60000102b5c0 'UISV-spacing' V:[UILabel:0x7f9b6ad49320]-(8)-[UILabel:0x7f9b6ad4c930]   (active)>",
    "<NSLayoutConstraint:0x60000102b610 'UISV-spacing' V:[UILabel:0x7f9b6ad4c930]-(8)-[UILabel:0x7f9b6ad4ceb0]   (active)>",
    "<NSLayoutConstraint:0x60000101dd60 'UIView-Encapsulated-Layout-Height' circulars.CircularCollectionCell:0x7f9b6ad4c5f0.height == 44   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60000102b610 'UISV-spacing' V:[UILabel:0x7f9b6ad4c930]-(8)-[UILabel:0x7f9b6ad4ceb0]   (active)>

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

Code:

class TestController: UIViewController {
    
    lazy var collectionView: UICollectionView = {
        let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        let layout = UICollectionViewCompositionalLayout.list(using: config)
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.translatesAutoresizingMaskIntoConstraints = false
        cv.backgroundColor = .systemBackground
        cv.delegate = self
        return cv
    }()
    
    var dataSource: UICollectionViewDiffableDataSource<Section, Circular>?
    
    override func viewDidLoad() {
        super.viewDidLoad()
      
        setupViews()
        setupData()
    }
    
    func setupViews() {
        view.backgroundColor = .systemBackground
        navigationItem.title = "CV"
        
        view.addSubview(collectionView)
        collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        setupCell()
    }
    
    func setupCell() {
        //No constraint conflict for this cell section
        let registration = UICollectionView.CellRegistration<UICollectionViewListCell, Circular> { (cell, indexPath, circular) in
            var content = cell.defaultContentConfiguration()
            content.text = circular.title
            cell.contentConfiguration = content
        }
        
        //Constraint conflict for this cell/section.
        let cirRegistration = UICollectionView.CellRegistration<CircularCollectionCell, Circular> { (cell, indexPath, circular) in
            cell.circular = circular
        }
        
        dataSource = UICollectionViewDiffableDataSource<Section, Circular>(collectionView: collectionView) { (collectionView, indexPath, circular) -> UICollectionViewCell? in
            
            if indexPath.section == 0 {
                return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: circular)

            } else {
                return collectionView.dequeueConfiguredReusableCell(using: cirRegistration, for: indexPath, item: circular)
            }
            
        }
    }
    
    func setupData() {
        let circulars = [
            //Setup circulars
        ]
        
        populate(circulars: circulars)
    }
    
    func populate(circulars: [Circular]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Circular>()
        snapshot.appendSections([.main, .second])
        snapshot.appendItems(circulars, toSection: .main)
        snapshot.appendItems(Array(circulars.prefix(2)), toSection: .second)
        dataSource?.apply(snapshot)
    }    
}

extension TestController {
    enum Section {
        case main, second
    }
}

//At custom CircularCollectionCell
class CircularCollectionCell: UICollectionViewListCell {
        
    var companyLabel: UILabel = {
        let l = UILabel()
        l.translatesAutoresizingMaskIntoConstraints = false
        l.numberOfLines = 0
        l.font = UIFont.systemFont(ofSize: 15, weight: .heavy)
        return l
    }()
    
    var titleLabel: UILabel = {
        let l = UILabel()
        l.translatesAutoresizingMaskIntoConstraints = false
        l.numberOfLines = 0
        l.font = UIFont.systemFont(ofSize: 17, weight: .medium)
        return l
    }()
    
    var dateLabel: UILabel = {
        let l = UILabel()
        l.translatesAutoresizingMaskIntoConstraints = false
        l.numberOfLines = 0
        l.font = UIFont.systemFont(ofSize: 14, weight: .light)
        l.textColor = .systemGray
        return l
    }()
    
    var circular: Circular? {
        didSet {
            if let circular = circular {
               //Setup UI
               dateLabel.text = circular.date
            }
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupViews() {
        
        let stackView = UIStackView(arrangedSubviews: [
            companyLabel,
            titleLabel,
            dateLabel
        ])
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.spacing = 8
        
        addSubview(stackView)
        stackView.topAnchor.constraint(equalTo: topAnchor, constant: 16).isActive = true
        stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16).isActive = true
        stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16).isActive = true
        stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16).isActive = true
    }

}
2 Answers

In your UICollectionViewCell subclass init, add the following line:

contentView.autoresizingMask = .flexibleHeight

This will make it so that the contentView allows heights greater than 44 and will no longer have conflicting height constraints.

One way to avoid the conflicting constraints issue is to give a constraint, which is responsible for the height i.e. topAnchor, a higher priority:

let topAnchorConstraint = stackView.topAnchor.constraint(equalTo: topAnchor, constant: 16)
topAnchorConstraint.priority = .defaultHigh // Also used sometimes: UILayoutPriority(999)
topAnchorConstraint.isActive = true

Because this constraint is now of a higher priority, UIView-Encapsulated-Layout-Height is overruled and the conflict is resolvable.

Related