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:
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 UICollectionViewCell
s). 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:
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.