UICollectionView, full width cells, allow autolayout dynamic height? UICollectionView, full width cells, allow autolayout dynamic height? ios ios

UICollectionView, full width cells, allow autolayout dynamic height?


1. Solution for iOS 13+

With Swift 5.1 and iOS 13, you can use Compositional Layout objects in order to solve your problem.

The following complete sample code shows how to display multiline UILabel inside full-width UICollectionViewCell:

CollectionViewController.swift

import UIKitclass CollectionViewController: UICollectionViewController {    let items = [        [            "Lorem ipsum dolor sit amet.",            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",        ],        [            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",        ],        [            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",            "Lorem ipsum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",        ]    ]    override func viewDidLoad() {        super.viewDidLoad()        let size = NSCollectionLayoutSize(            widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),            heightDimension: NSCollectionLayoutDimension.estimated(44)        )        let item = NSCollectionLayoutItem(layoutSize: size)        let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1)        let section = NSCollectionLayoutSection(group: group)        section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)        section.interGroupSpacing = 10        let headerFooterSize = NSCollectionLayoutSize(            widthDimension: .fractionalWidth(1.0),            heightDimension: .absolute(40)        )        let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(            layoutSize: headerFooterSize,            elementKind: "SectionHeaderElementKind",            alignment: .top        )        section.boundarySupplementaryItems = [sectionHeader]        let layout = UICollectionViewCompositionalLayout(section: section)        collectionView.collectionViewLayout = layout        collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")        collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")    }    override func numberOfSections(in collectionView: UICollectionView) -> Int {        return items.count    }    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        return items[section].count    }    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell        cell.label.text = items[indexPath.section][indexPath.row]        return cell    }    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView", for: indexPath) as! HeaderView        headerView.label.text = "Header"        return headerView    }    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {        super.viewWillTransition(to: size, with: coordinator)        coordinator.animate(alongsideTransition: { context in            self.collectionView.collectionViewLayout.invalidateLayout()        }, completion: nil)    }}

HeaderView.swift

import UIKitclass HeaderView: UICollectionReusableView {    let label = UILabel()    override init(frame: CGRect) {        super.init(frame: frame)        backgroundColor = .magenta        addSubview(label)        label.translatesAutoresizingMaskIntoConstraints = false        label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true        label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true    }    required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")    }}

CustomCell.swift

import UIKitclass CustomCell: UICollectionViewCell {    let label = UILabel()    override init(frame: CGRect) {        super.init(frame: frame)        label.numberOfLines = 0        backgroundColor = .orange        contentView.addSubview(label)        label.translatesAutoresizingMaskIntoConstraints = false        label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true        label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true        label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true        label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true    }    required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")    }}

Expected display:

enter image description here


2. Solution for iOS 11+

With Swift 5.1 and iOS 11, you can subclass UICollectionViewFlowLayout and set its estimatedItemSize property to UICollectionViewFlowLayout.automaticSize (this tells the system that you want to deal with autoresizing UICollectionViewCells). You'll then have to override layoutAttributesForElements(in:) and layoutAttributesForItem(at:) in order to set cells width. Lastly, you'll have to override your cell's preferredLayoutAttributesFitting(_:) method and compute its height.

The following complete code shows how to display multiline UILabel inside full-width UIcollectionViewCell (constrained by UICollectionView's safe area and UICollectionViewFlowLayout's insets):

CollectionViewController.swift

import UIKitclass CollectionViewController: UICollectionViewController {    let items = [        [            "Lorem ipsum dolor sit amet.",            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",        ],        [            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",        ],        [            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",            "Lorem ipsum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",        ]    ]    let customFlowLayout = CustomFlowLayout()    override func viewDidLoad() {        super.viewDidLoad()        customFlowLayout.sectionInsetReference = .fromContentInset // .fromContentInset is default        customFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize        customFlowLayout.minimumInteritemSpacing = 10        customFlowLayout.minimumLineSpacing = 10        customFlowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)        customFlowLayout.headerReferenceSize = CGSize(width: 0, height: 40)        collectionView.collectionViewLayout = customFlowLayout        collectionView.contentInsetAdjustmentBehavior = .always        collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")        collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")    }    override func numberOfSections(in collectionView: UICollectionView) -> Int {        return items.count    }    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        return items[section].count    }    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell        cell.label.text = items[indexPath.section][indexPath.row]        return cell    }    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView", for: indexPath) as! HeaderView        headerView.label.text = "Header"        return headerView    }}

CustomFlowLayout.swift

import UIKitfinal class CustomFlowLayout: UICollectionViewFlowLayout {    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {        let layoutAttributesObjects = super.layoutAttributesForElements(in: rect)?.map{ $0.copy() } as? [UICollectionViewLayoutAttributes]        layoutAttributesObjects?.forEach({ layoutAttributes in            if layoutAttributes.representedElementCategory == .cell {                if let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame {                    layoutAttributes.frame = newFrame                }            }        })        return layoutAttributesObjects    }    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {        guard let collectionView = collectionView else {            fatalError()        }        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {            return nil        }        layoutAttributes.frame.origin.x = sectionInset.left        layoutAttributes.frame.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right        return layoutAttributes    }}

HeaderView.swift

import UIKitclass HeaderView: UICollectionReusableView {    let label = UILabel()    override init(frame: CGRect) {        super.init(frame: frame)        backgroundColor = .magenta        addSubview(label)        label.translatesAutoresizingMaskIntoConstraints = false        label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true        label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true    }    required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")    }}

CustomCell.swift

import UIKitclass CustomCell: UICollectionViewCell {    let label = UILabel()    override init(frame: CGRect) {        super.init(frame: frame)        label.numberOfLines = 0        backgroundColor = .orange        contentView.addSubview(label)        label.translatesAutoresizingMaskIntoConstraints = false        label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true        label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true        label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true        label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true    }    required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")    }    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {        let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)        layoutIfNeeded()        layoutAttributes.frame.size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)        return layoutAttributes    }}

Here are some alternative implementations for preferredLayoutAttributesFitting(_:):

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {    let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)    layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)    return layoutAttributes}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {    label.preferredMaxLayoutWidth = layoutAttributes.frame.width    layoutAttributes.frame.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height    return layoutAttributes}

Expected display:

enter image description here


Problem

You are looking for automatic height and also want to have full in width, it is not possible to get both in using UICollectionViewFlowLayoutAutomaticSize.

You want to do using UICollectionView so below is the solution for you.

Solution

Step-I: Calculate the expected height of Cell

1. If you have only UILabel in CollectionViewCell than set the numberOfLines=0 and that calculated the expected height of UIlable, pass the all three paramters

func heightForLable(text:String, font:UIFont, width:CGFloat) -> CGFloat {    // pass string, font, LableWidth      let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))     label.numberOfLines = 0     label.lineBreakMode = NSLineBreakMode.byWordWrapping     label.font = font     label.text = text     label.sizeToFit()     return label.frame.height}

2. If your CollectionViewCell contains only UIImageView and if it's is supposed to be dynamic in Height than you need to get the height of UIImage (your UIImageView must have AspectRatio constraints)

// this will give you the height of your Imagelet heightInPoints = image.size.heightlet heightInPixels = heightInPoints * image.scale

3. If it contains both than calculated their height and add them together.

STEP-II: Return the Size of CollectionViewCell

1. Add UICollectionViewDelegateFlowLayout delegate in your viewController

2. Implement the delegate method

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {    // This is just for example, for the scenario Step-I -> 1     let yourWidthOfLable=self.view.size.width    let font = UIFont(name: "Helvetica", size: 20.0)    var expectedHeight = heightForLable(array[indePath.row], font: font, width:yourWidthOfLable)    return CGSize(width: view.frame.width, height: expectedHeight)}

I hope this will help you out.


There are couple of ways you could tackle this problem.

One way is you can give the collection view flow layout an estimated size and calculating the cell size.

Note: As mentioned in the comments below, as of iOS 10 you no longer need to provide and estimated size to trigger the call to a cells func preferredLayoutAttributesFitting(_ layoutAttributes:). Previously (iOS 9) would require you to provide an estimated size if you wanted to query a cells preferredLayoutAttributes.

(assuming you are using storyboards and the collection view is connected via IB)

override func viewDidLoad() {    super.viewDidLoad()    let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout    layout?.estimatedItemSize = CGSize(width: 375, height: 200) // your average cell size}

For simple cells that will usually be enough. If size is still incorrect, in the collection view cell you can override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, which will give you more fine grain control over the cell size. Note: You will still need to give the flow layout an estimated size.

Then override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes to return the correct size.

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {    let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)    let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)    let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)    let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)    autoLayoutAttributes.frame = autoLayoutFrame    return autoLayoutAttributes}

Alternatively, instead you can use a sizing cell to calculate the size of the cell in the UICollectionViewDelegateFlowLayout.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {    let width = collectionView.frame.width    let size = CGSize(width: width, height: 0)    // assuming your collection view cell is a nib    // you may also instantiate a instance of our cell from a storyboard    let sizingCell = UINib(nibName: "yourNibName", bundle: nil).instantiate(withOwner: nil, options: nil).first as! YourCollectionViewCell    sizingCell.autoresizingMask = [.flexibleWidth, .flexibleHeight]    sizingCell.frame.size = size    sizingCell.configure(with: object[indexPath.row]) // what ever method configures your cell    return sizingCell.contentView.systemLayoutSizeFitting(size, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)}

While these are not perfect production ready examples, they should get you started in the right direction. I can not say this is the best practice, but this works for me, even with fairly complex cells containing multiple labels, that may or may not wrap to multiple lines.