Reputation: 12582
Note that Apple now have UICollectionViewCompositionalLayout, which is trivial to use.
(Essentially just set fractionalWidth to 1.)
All previous apple systems for sizing collection views were garbage, and they finally fixed it with UICollectionViewCompositionalLayout.
In a vertical UICollectionView
,
Is it possible to have full-width cells, but, allow the dynamic height to be controlled by autolayout?
This strikes me as perhaps the "most important question in iOS with no really good answer."
Upvotes: 196
Views: 153070
Reputation: 469
The correct modern solution in 2023 is to use a compositional layout, which has been documented in various answers on this post.
For strong documentation on any plausible example, consult this guide by Apple with associated source code:
Please note that the estimated size should neither be too small or too large otherwise it'll look a bit weird on load.
When reloading layout or switching orientation, invalidate the layout using:
self.collectionView.collectionViewLayout.invalidateLayout()
Other Options
It is also possible to do it with preferredLayoutAttributesFitting after updating cells and invalidating the content size but, this leads to a very janky solution that is not fluid nor premium.
Strongly recommend you follow an example in this post or on Apple's link.
Upvotes: 2
Reputation: 9431
When using UICollectionViewCompositionalLayout
make sure you set both itemSize
and groupSize
height to .estimated
:
My problem was that I was using .fractional(1)
for item height
and .estimated(44)
for group
and it didn't work.
extension TasksController {
static func createLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout { (sectionIndex: Int, _ NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let height: CGFloat = 44
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(height))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(height))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)
let section = NSCollectionLayoutSection(group: group)
return section
}
}
}
Upvotes: 1
Reputation: 468
I have seen many complex answers for cells with dynamic height. However after few google searches i found this simple answer.
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(UINib(nibName: "LabelOnlyCell", bundle: nil), forCellWithReuseIdentifier: "LabelOnlyCell")
if let collectionViewLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
Important Part
collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
LabelOnlyCell class for reference
class LabelOnlyCell: UICollectionViewCell {
@IBOutlet weak var labelHeading: UILabel!
@IBOutlet weak var parentView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.parentView.translatesAutoresizingMaskIntoConstraints = false
self.parentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
}
}
Upvotes: 1
Reputation: 844
My solution is lifted from https://www.advancedswift.com/autosizing-full-width-cells/
Add the following inside your Custom Cell Class:
class MyCustomCell: UICollectionViewCell {
...
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
var targetSize = targetSize
targetSize.height = CGFloat.greatestFiniteMagnitude
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
return size
}
}
And the following in the ViewDidLoad of the ViewController containing your Collection View:
override func viewDidLoad() {
super.viewDidLoad()
...
let cellWidth = 200 // whatever your cell width is
let layout = myCustomCollectionView.collectionViewLayout
if let flowLayout = layout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(
width: cellWidth,
height: 200 //an estimated height, but this will change when the cell is created
)
}
}
This is the simplest/shortest solution I've found.
Upvotes: -2
Reputation: 1475
There are a couple of ways you could tackle this problem.
One way is you can give the collection view flow layout an estimated size and use systemLayoutSizeFitting
to calculate the cell size.
Note: As mentioned in the comments below, as of iOS 10 you no longer need to provide an estimated size to trigger the call to func preferredLayoutAttributesFitting(_ layoutAttributes:)
on the cell. Previously (iOS 9) would require you to provide an estimated size if you wanted preferredLayoutAttributes to be called.
(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 the 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
. If you use this method consider caching the size for performance.
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 an instance of your cell if it doesn't use a Nib
// let sizingCell = MyCollectionViewCell()
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.
Upvotes: 24
Reputation: 9121
If you're using iOS 14 or newer, then you can use the UICollectionLayoutListConfiguration
API, which makes it possible to use UICollectionView
as a single column tableview, including features like horizontal swipe context menus and auto-height cells.
var collectionView: UICollectionView! = nil
override func viewDidLoad() {
super.viewDidLoad()
let config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
}
More info, including an example project, about how to configure cells and the data source can be found in this article from Apple: Implementing Modern Collection Views, especially the section that starts with Create a Simple List Layout.
The example projects contains a controller named ConferenceNewsFeedViewController which shows how to configure auto-height cells based on auto-layout.
Upvotes: 15
Reputation: 535
I followed this answer, simple and achieves the goal. Plus it gives me the benefit to adjust my view for iPad unlike the selected answer.
However I wanted the width of the cell to change according to device width, so I did not add a width constraint, and tweaked the preferredLayoutAttributesFitting
method to be like this:
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let targetSize = CGSize(width: UIScreen.main.bounds.size.width / ((UIDevice.current.userInterfaceIdiom == .phone) ? 1 : 2), height: 0);
layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel);
return layoutAttributes;
}
Upvotes: -1
Reputation: 1442
I also faced with dynamic cell's height issue and could resolve the issue using Autolayout and manual height calculation. No need to use estimated sizes, instantiating or creating cells.
Also this solution provides handling of multiline labels. It's based on calculating of subviews height of all subviews in a cell.
extension CollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let contentHorizontalSpaces = collectionLayout.minimumInteritemSpacing
+ collectionLayout.sectionInset.left
+ collectionLayout.sectionInset.right
let newCellWidth = (collectionView.bounds.width - contentHorizontalSpaces) / 2
let newHeight = Cell.getProductHeightForWidth(props: data[indexPath.row], width: newCellWidth)
return CGSize(width: newCellWidth, height: newHeight)
}
}
UICollectionViewDelegateFlowLayout uses size of the cell which is calculated in getProductHeightForWidth
method:
extension Cell {
class func getProductHeightForWidth(props: Props, width: CGFloat) -> CGFloat {
// magic numbers explanation:
// 16 - offset between image and price
// 22 - height of price
// 8 - offset between price and title
var resultingHeight: CGFloat = 16 + 22 + 8
// get image height based on width and aspect ratio
let imageHeight = width * 2 / 3
resultingHeight += imageHeight
let titleHeight = props.title.getHeight(
font: .systemFont(ofSize: 12), width: width
)
resultingHeight += titleHeight
return resultingHeight
}
}
I created a story here: https://volodymyrrykhva.medium.com/uicollectionview-cells-with-dynamic-height-using-autolayout-a4e346b7bd2a
Full code on solution is on GitHub: https://github.com/ascentman/DynamicHeightCells
Upvotes: 1
Reputation: 518
AutoLayout can be used for auto-sizing of cells in CollectionView in 2 easy steps:
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
collectionView(:cellForItemAt:)
to limit the width of contentView to width of collectionView.class ViewController: UIViewController, UICollectionViewDataSource {
...
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! MultiLineCell
cell.textView.text = dummyTextMessages[indexPath.row]
cell.maxWidth = collectionView.frame.width
return cell
}
...
}
class MultiLineCell: UICollectionViewCell{
....
var maxWidth: CGFloat? {
didSet {
guard let maxWidth = maxWidth else {
return
}
containerViewWidthAnchor.constant = maxWidth
containerViewWidthAnchor.isActive = true
}
}
....
}
That's it you'll get the desired result. Refer following gists for full code:
Reference/Credits :
Upvotes: 1
Reputation: 92409
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:
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:
Upvotes: 245
Reputation: 1243
WORKING!!! Tested on IOS:12.1 Swift 4.1
I have a very simple solution that just works with no constraint breaking.
My ViewControllerClass
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
let cellId = "CustomCell"
var source = ["nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,","nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,","nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,"]
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.register(UINib.init(nibName: cellId, bundle: nil), forCellWithReuseIdentifier: cellId)
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.source.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as? CustomCell else { return UICollectionViewCell() }
cell.setData(data: source[indexPath.item])
return cell
}
}
CustomCell class:
class CustomCell: UICollectionViewCell {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var widthConstraint: NSLayoutConstraint!
override func awakeFromNib() {
super.awakeFromNib()
self.widthConstraint.constant = UIScreen.main.bounds.width
}
func setData(data: String) {
self.label.text = data
}
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
return contentView.systemLayoutSizeFitting(CGSize(width: self.bounds.size.width, height: 1))
}
}
Main ingredient is the systemLayoutSizeFitting function in Customcell. And also we have to set width of the view inside Cell with constraints.
Upvotes: 13
Reputation: 1713
From iOS 10, we've got new API on flow layout to do that.
All you have to do is set your flowLayout.estimatedItemSize
to a new constant, UICollectionViewFlowLayoutAutomaticSize
.
Upvotes: -1
Reputation: 216
Set estimatedItemSize
of your flow layout:
collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
Define a width constraint in the cell and set it to be equal to superview's width:
class CollectionViewCell: UICollectionViewCell {
private var widthConstraint: NSLayoutConstraint?
...
override init(frame: CGRect) {
...
// Create width constraint to set it later.
widthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0)
}
override func updateConstraints() {
// Set width constraint to superview's width.
widthConstraint?.constant = superview?.bounds.width ?? 0
widthConstraint?.isActive = true
super.updateConstraints()
}
...
}
Tested on iOS 11.
Upvotes: 7
Reputation: 9039
Per my comment on Eric's answer, my solution is very similar to his, but I had to add a constraint in preferredSizeFor... in order to constrain to the fixed dimension.
override func systemLayoutSizeFitting(
_ targetSize: CGSize, withHorizontalFittingPriority
horizontalFittingPriority: UILayoutPriority,
verticalFittingPriority: UILayoutPriority) -> CGSize {
width.constant = targetSize.width
let size = contentView.systemLayoutSizeFitting(
CGSize(width: targetSize.width, height: 1),
withHorizontalFittingPriority: .required,
verticalFittingPriority: verticalFittingPriority)
print("\(#function) \(#line) \(targetSize) -> \(size)")
return size
}
This question has a number of duplicates, I answered it in detail here, and provided a working sample app here.
Upvotes: 4
Reputation: 6419
Personally I found the best ways to have a UICollectionView where AutoLayout determines the size while each Cell can have a different size is to implement the UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath function while using an actual Cell to measure the size.
I talked about this in one of my blog posts
Hopefully this one will help you to achieve what you want. I'm not 100% sure but I believe unlike UITableView where you can actually have a fully automatic height of cells by using AutoLayout inconjunction with
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44
UICollectionView does not have such a way of letting AutoLayout determine the size because UICollectionViewCell does not necessarily fills the whole width of the screen.
But here is a question for you: If you need full screen width cells, why do you even bother using the UICollectionView over a good old UITableView which comes with the auto sizing cells?
Upvotes: 3
Reputation: 3060
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 Image
let heightInPoints = image.size.height
let 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.
Upvotes: 32
Reputation: 4551
You have to add width constraint to CollectionViewCell
class SelfSizingCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
}
}
Upvotes: 6
Reputation: 964
I found a pretty easy solution for that issue: Inside of my CollectionViewCell I got a UIView() which is actually just a background. To get full width I just set the following Anchors
bgView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width - 30).isActive = true // 30 is my added up left and right Inset
bgView.topAnchor.constraint(equalTo: topAnchor).isActive = true
bgView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
bgView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
bgView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
The "magic" happens in the first line. I set the widthAnchor dynamically to the width of the screen. Also important is to subtract the insets of your CollectionView. Otherwise the cell won't show up. If you don't want to have such a background view, just make it invisible.
The FlowLayout uses the following settings
layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
Result is a full width sized cell with dynamic height.
Upvotes: 19
Reputation: 69
None of the solutions were working for me as I need dynamic width to adapt between iPhones width.
class CustomLayoutFlow: UICollectionViewFlowLayout {
override init() {
super.init()
minimumInteritemSpacing = 1 ; minimumLineSpacing = 1 ; scrollDirection = .horizontal
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
minimumInteritemSpacing = 1 ; minimumLineSpacing = 1 ; scrollDirection = .horizontal
}
override var itemSize: CGSize {
set { }
get {
let width = (self.collectionView?.frame.width)!
let height = (self.collectionView?.frame.height)!
return CGSize(width: width, height: height)
}
}
}
class TextCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var textView: UITextView!
override func prepareForReuse() {
super.prepareForReuse()
}
}
class IntroViewController: UIViewController, UITextViewDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UINavigationControllerDelegate {
@IBOutlet weak var collectionViewTopDistanceConstraint: NSLayoutConstraint!
@IBOutlet weak var collectionViewTopDistanceConstraint: NSLayoutConstraint!
@IBOutlet weak var collectionView: UICollectionView!
var collectionViewLayout: CustomLayoutFlow!
override func viewDidLoad() {
super.viewDidLoad()
self.collectionViewLayout = CustomLayoutFlow()
self.collectionView.collectionViewLayout = self.collectionViewLayout
}
override func viewWillLayoutSubviews() {
self.collectionViewTopDistanceConstraint.constant = UIScreen.main.bounds.height > 736 ? 94 : 70
self.view.layoutIfNeeded()
}
}
Upvotes: 0
Reputation: 8192
On your viewDidLayoutSubviews
, set the estimatedItemSize
to full width (layout refers to the UICollectionViewFlowLayout object):
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.bounds.size.width, height: 120)
}
On your cell, be sure that your constraints touch both the top and bottom of the cell (the following code uses Cartography to simplify setting the constraints but you can do it with NSLayoutConstraint or IB if you want):
constrain(self, nameLabel, valueLabel) { view, name, value in
name.top == view.top + 10
name.left == view.left
name.bottom == view.bottom - 10
value.right == view.right
value.centerY == view.centerY
}
Voila, you cells will now autogrow in height!
Upvotes: 1
Reputation: 10460
Not sure if this qualifies as a "really good answer", but it's what I'm using to accomplish this. My flow layout is horizontal, and I'm trying to make the width adjust with autolayout, so it's similar to your situation.
extension PhotoAlbumVC: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// My height is static, but it could use the screen size if you wanted
return CGSize(width: collectionView.frame.width - sectionInsets.left - sectionInsets.right, height: 60)
}
}
Then in the view controller where the autolayout constraint gets modified, I fire off an NSNotification.
NotificationCenter.default.post(name: NSNotification.Name("constraintMoved"), object: self, userInfo: nil)
In my UICollectionView subclass, I listen for that notification:
// viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(handleConstraintNotification(notification:)), name: NSNotification.Name("constraintMoved"), object: nil)
and invalidate the layout:
func handleConstraintNotification(notification: Notification) {
self.collectionView?.collectionViewLayout.invalidateLayout()
}
This causes sizeForItemAt
to be called again using the collection view's new size. In your case, it should be able to update given the new constraints available in the layout.
Upvotes: 2