Search code examples
iosuicollectionviewautolayoutuicollectionviewlayout

Why is layout of collectionView cell diverse when it is supposed to be the same?


When i was creating my collection view, i defined cell to be the same with the same layout

class FriendsController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
    fileprivate let cellId = "cellId"
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Recent"
        collectionView?.backgroundColor = UIColor.white
        collectionView?.register(FriendCell.self, forCellWithReuseIdentifier: cellId)
        collectionView?.alwaysBounceVertical = true
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 2
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        return collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: view.frame.width, height: 100)
    }


}

class FriendCell: BaseCell {

    override func setupViews() {

        addSubview(profileImageView)
        addSubview(dividerLineView)
        profileImageView.image = UIImage(named: "zuckprofile")

        setupContainerView()

        addConstraintsWithFormat( "H:|-12-[v0(68)]|", views: profileImageView)
        addConstraintsWithFormat( "V:[v0(68)]", views: profileImageView)
        addConstraint(NSLayoutConstraint(item: profileImageView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0))
        addConstraintsWithFormat( "H:|-82-[v0]|", views: dividerLineView)
        addConstraintsWithFormat( "V:[v0(1)]|", views: dividerLineView)



    }
    func setupContainerView() {
        let containerView = UIView()
        addSubview(containerView)

        addConstraintsWithFormat("H:|-90-[v0]|", views: containerView)
        addConstraintsWithFormat(  "V:[v0(50)]", views: containerView)
        addConstraint(NSLayoutConstraint(item: containerView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0))
        containerView.addSubview(nameLabel)
        containerView.addSubview(messageLabel)
        containerView.addSubview(timelabel)
        containerView.addConstraintsWithFormat( "H:|[v0][v1]|", views: nameLabel, timelabel )
        containerView.addConstraintsWithFormat( "V:|[v0][v1(24)]|", views: nameLabel, messageLabel)
        containerView.addConstraintsWithFormat( "H:|[v0]-12-|", views: messageLabel )
        containerView.addConstraintsWithFormat("V:|[v0(20)]|", views: timelabel)
    }

}

extension UIView {
    func addConstraintsWithFormat(_ format: String , views: UIView...) {
        var viewsDictionary = [String: UIView]()
        for (index, view) in views.enumerated() {
            let key = "v\(index)"
            viewsDictionary[key] = view
            view.translatesAutoresizingMaskIntoConstraints = false
        }
        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
    }
}

So as it seems from the code everything in cells shout be the same. With the same layout, constraints etc Then i got this terrible result.enter image description here

Please maybe someone knows why this behavior take place here?


Solution

  • I believe the problem lies in ambiguity between nameLabel and timeLabel intrinsic size and their content hugging priority and content compression resistance priority (check this article).

    You have two options, either set following horizontal compression resistance and hugging priority for both labels in setupContainerView:

    nameLabel.setContentHuggingPriority(UILayoutPriority.defaultLow, for: .horizontal)
    nameLabel.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: .horizontal)
    timeLabel.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal)
    timeLabel.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh, for: .horizontal)
    

    Which will make the timeLabel to keep the size according to its content and the nameLabel will take the rest of the content (meaning that if the time is not set, the timeLabel will be shrinked to width 0, and the rest will be taken up by nameLabel).

    Or set an explicit width for one of those labels, e.g.:

    timeLabel.widthAnchor.constraint(equalToConstant: 40).isActive = true
    

    EDIT

    According to Apple docs,

    When an ambiguous layout occurs at runtime, Auto Layout chooses one of the possible solutions to use. This means the layout may or may not appear as you expect. Furthermore, there are no warnings written to the console, and there is no way to set a breakpoint for ambiguous layouts.

    Or other doc:

    When stretching a series of views to fill a space, if all the views have an identical content-hugging priority, the layout is ambiguous. Auto Layout doesn’t know which view should be stretched.

    None of the docs I was able to find tells us, how does the Auto Layout solve this issues - and I believe the reason is to make sure we won't be relying on this and rather explicitly set the priorities. Therefore, the label that gets stretched (even the size to which it is stretched) can be really chosen by any random algorithm. Based from your result we can at best conclude, that probably it is not based on the order in which labels were added as subviews.