Reputation: 6560
I have a working list with UICollectionViewController (but the issue might be relevant for bare UICollectionView, I have not tested). It is working like Image below, but the issue is, the width varies contents.
I want the width to fill the width and let the height calculate itself based on contents. So far I have not been able to put a coherent solution. I have tried to override FlowLayout but then I have to pass the height of the cell which I don't know at that moment.
What is the solution that iOS devs use to solve such a problem. It looks to me common sense thing but apple seems to have complicated this one for no reason.
Upvotes: 2
Views: 661
Reputation: 6732
Swift 5 example of full width cells when standard UICollectionViewFlowLayout
is used.
Idea behind:
UICollectionViewFlowLayout
that we want to have self-sizing cells.preferredLayoutAttributesFitting(...)
method. Then it performs desired size calculation using passed width. Desired size calculated using systemLayoutSizeFitting(width: ...)
.class MyVievControler: UICollectionViewController {
enum CellId: String {
case red, green, blue
}
let items = repeatElement([CellId.red, .green, .blue], count: 40).reduce(into: []) { $0 += $1 }
init() {
let layout = UICollectionViewFlowLayout()
// We want self-sizing cells
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
super.init(collectionViewLayout: layout)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cellId = items[indexPath.row]
switch cellId {
case .red:
let cell = collectionView.dequeueReusableCell(MyCellRed.self, indexPath: indexPath)
cell.contentWidth = collectionView.bounds.width
cell.text = indexPath.item % 2 == 0 ? StubObject().text.x128 : StubObject().text.x32
return cell
case .green:
let cell = collectionView.dequeueReusableCell(MyCellGreen.self, indexPath: indexPath)
cell.contentWidth = collectionView.bounds.width
return cell
case .blue:
let cell = collectionView.dequeueReusableCell(MyCellBlue.self, indexPath: indexPath)
cell.contentWidth = collectionView.bounds.width
return cell
}
}
}
private class MyCellRed: UICollectionViewCell {
var contentWidth: CGFloat = 100
private lazy var label = UILabel()
var text: String? {
didSet {
label.text = text
}
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
var newFrame = layoutAttributes.frame
// For better performance this size need to be cached as long as Cell content/model is not changed.
let desiredSize = systemLayoutSizeFitting(width: contentWidth, verticalFitting: .fittingSizeLevel)
newFrame.size = CGSize(width: contentWidth, height: desiredSize.height)
layoutAttributes.frame = newFrame
log.debug("Red: Calculated frame: \(newFrame). Desired size: \(desiredSize)")
return layoutAttributes
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundView = UIView(backgroundColor: .red)
layer.setBorder(width: 0.5)
contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
anchor.pin.toBounds(insets: 8, label).activate()
label.layer.borderWidth = 1
label.numberOfLines = 0
label.textColor = .white
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private class MyCellGreen: UICollectionViewCell {
var contentWidth: CGFloat = 100
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
var newFrame = layoutAttributes.frame
newFrame.size = CGSize(width: contentWidth, height: 40)
layoutAttributes.frame = newFrame
log.debug("Green: Calculated frame: \(newFrame)")
return layoutAttributes
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundView = UIView(backgroundColor: .green)
layer.setBorder(width: 0.5)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private class MyCellBlue: UICollectionViewCell {
var contentWidth: CGFloat = 100
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
var newFrame = layoutAttributes.frame
newFrame.size = CGSize(width: contentWidth, height: 100)
layoutAttributes.frame = newFrame
log.debug("Blue: Calculated frame: \(newFrame)")
return layoutAttributes
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundView = UIView(backgroundColor: .blue)
layer.setBorder(width: 0.5)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Result:
Upvotes: 1
Reputation: 6560
After reading this Article (Thanks to @DonMag), I was able to achieve what I wanted. So basically you extend the Cell class, and do some changes to CollectionView and its flowLayout. Here are the classes
FullWidthCollectionViewCell: All your cells in need of full width should extend it instead of UICollectionViewCell
class FullWidthCollectionViewCell: UICollectionViewCell
{
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
var modifiedTargetSize = targetSize
modifiedTargetSize.height = CGFloat.greatestFiniteMagnitude
let size = super.systemLayoutSizeFitting(
modifiedTargetSize,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
return size
}
}
Add extension to UICollectionView class. I have single file for all my view extensions but you can put it in the same file as CollectionView code
extension UICollectionView {
var widestCellWidth: CGFloat {
let insets = contentInset.left + contentInset.right
return bounds.width - insets
}
}
Finally in your CollectionView code. In my case I was using UICollectionViewController so All is set for me. If you are using UICollectionView as view in some custom UIViewController then you must adjust the code accordingly
class MyCollectionViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
let layout = collectionView.collectionViewLayout
if let flowLayout = layout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(
width: collectionView.widestCellWidth,
// Make the height a reasonable estimate to
// ensure the scroll bar remains smooth
height: 200
)
}
}
//Delegate and other methods here
}
That is all to enjoy. Again thanks to @DonMag for the link!
Upvotes: 1