How to give a collection view section header dynamic height
None of the answers given so far are good practices since calculating the size manually or by rendering a label offscreen are both inefficient and violate the single source of truth principle.
Instances of UICollectionReusableView
have the preferredLayoutAttributesFitting()
method. When the cell becomes displayed, this method will be called. In this method, if you use Auto Layout, you ask the whole cell for its systemLayoutSizeFitting()
and then modify the attributes. The layout will then be responsible to apply them via layout invalidation.
UICollectionView dynamic header size calculation
I finally solved the problem by constraining the width:
reusableView.translatesAutoresizingMaskIntoConstraints = true
reusableView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
Might be that you also have to play around with content compression resistances.
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 UIKit
class 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 UIKit
class 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 UIKit
class 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 UIKit
class 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 UIKit
final 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 UIKit
class 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 UIKit
class 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:
Related Topics
How to Create a Hex Color String Uicolor Initializer in Swift
Install Apps on Device Without Itunes
Can't Pass Data Correctly to Modal Presentation Using Foreach and Coredata in Swiftui
Firebase Cloud Messaging Doesn't Create Push Notifications But Gets Information
Wkwebview Content Loaded Function Never Get Called
Swift 2.0 Sorting Array of Objects by Property
Uicollectionview's Cell Registerclass in Swift
Creating Framework That Requires (Depends On) Another Framework
Nscache Doesn't Work with All Images When Loading for the First Time
Swiftui - iOS 13 Uiviewrepresentable of Wkwebview Gets Thread 1: Exc_Breakpoint Crash
Mutating Self (Struct/Enum) Inside Escaping Closure in Swift 3.0
Convert Nsdata to Sockaddr Struct in Swift
How to Access Both Objective-C and Swift Classes from Same Storyboard
Button Action in Custom Uitableviewcell Affects Other Cells