Dynamic Cell Height for Custom Collection View Layout - ios

I have a collection view which makes use of a custom layout. I'm trying to calculate the height dynamically, but the problem is sizeForItemAt:indexPath is called before cellForItemAt:indexPath.
My cells get loaded in cellForItemAt. But since sizeForItemAt is called before cellForItemAt, then I can't use my calculated height.
I know with Apple's FlowLayout I can just set the estimatedItemSize for the layout. I'm not sure how to do it with a custom layout.
Please advise. Thank you!

I had the same problem, my app uses custom layout with dynamic height. I found that with custom layout that doesn't extend UICollectionViewFlowLayout or any other default layout, dynamic height will not work because as per Apple documentation (and like you probably noticed) with a completely custom layout you have to predefine all the cells X, Y, width, height before the cell load and before you even have data.
I changed my custom layout to subclass UICollectionViewFlowLayout and implemented UICollectionViewDelegateFlowLayout. When this method is called the cell did not load yet but the cell's data is available since I know what the cell should look like (assuming you are using cell prototypes) I could calculate the cell width and height using its data and index, something like this:
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// get the cell's data
if let data = self.fetchedResultsController.fetchedObjects![indexPath.row] as? YourDataType {
// carculate the cell width according to cell position
let cellWidth = carculateCellWidth(indexPath.row)
var cellHeight : CGFloat = 0
// assuming the cell have a label, set the label to have the same attributes as set in the storyboard or set programmatically
let label = UILabel()
label.numberOfLines = 0
label.font = UIFont.preferredFont(forTextStyle: .subheadline)
label.text = data.text
// carculate the height of the cell, assuming here the label width equal the cell width minus 10px left and right padding.
cellHeight += label.systemLayoutSizeFitting(CGSize(width:cellWidth-20, height: CGFloat(Float.greatestFiniteMagnitude))).height
return CGSize(width: cellWidth, height: cellHeight)
}
return .zero
}
This is not a very elegant solution but it works.

Related

Understanding UICollectionViewFlowLayout sizing and spacing

I am working with a UICollectionView using UICollectionViewFlowLayout and have some difficulties to understand item sizing an spacing. I know that there several methods to adapt sizing and spacing (using the delegate methods, overriding FlowLayout, etc.). However without understanding the logic behind these values in the first place, it is quite hard to adapt them properly.
The following results have been created a default UICollectionViewController with a default UICollectionViewCell without any subclasses. Only the following settings haven been made:
Specified the cell size to be 200 x 200 in IB
Placed a Label inside the cell and centered in vertically and horizontal
Code:
private let reuseIdentifier = "Cell"
class MyViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
var layout: UICollectionViewFlowLayout {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 5
layout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 20)
layout.sectionInsetReference = .fromContentInset
return layout
}
collectionView.collectionViewLayout = layout
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
/*func collectionView(_ collectionView : UICollectionView,layout collectionViewLayout:UICollectionViewLayout,sizeForItemAt indexPath:IndexPath) -> CGSize {
var width = view.frame.width
return CGSize(width: collectionVw.frame.size.height, height: collectionVw.frame.size.height)
}*/
}
Using different values for layout.minimumInteritemSpacing = 5 creates results I do not understand:
Why are the items 50 x 50px in size? I know that one can use ...sizeForItemAt to specify explicit dimensions. However, setting the size in IB should also work, shouldn't it? Why is the IB size of 200 x 200px ignored? Why 50x50, is this the default size or where is this specified? Solved in the answer by #Larme
I know that minimumInteritemSpacing does not set the explicit spacing but only a minimum. However, how is the value computed?
Why is the a spacing of 7.5px for a value of 5? Why result values 10-25 result all in the same a spacing of 26.5px?
So: How exactly are sizes and spacing computed here?
You have multiple questions (not really recommended), I'll answer what I can do.
Why are the items 50 x 50px in size? I know that one can use ...sizeForItemAt to specify explicit dimensions. However, setting the size in IB should also work, shouldn't it? Why is the IB size of 200 x 200px ignored? Why 50x50, is this the default size or where is this specified?
You are doing:
collectionView.collectionViewLayout = layout
Where layout is newly created.
You aren't using the previous settings in InterfaceBuilder, you are overriding them by code.
And your created for layout, its itemSize hasn't been set. And from the doc, if not set, it's 50x50.
Why are the items aligned to the left for these values and distribued evenly of the complete width for value of 30?
Are you sure about that? Default Layout (meaning, not a inherited from UICollectionViewFlowLayout) will behave like paragraphs styling.
I'll take the horizontal layout (same logic can be applied in vertical, but analogy with text paragraphs would be strange):
If you have multilines text, the first line would take as much width as possible, but the last line will not, keeping it "left" aligned.
For your spacing calculations, according to the doc of minimumInterItemSpacing:
For a vertically scrolling grid, this value represents the minimum spacing between items in the same row. For a horizontally scrolling grid, this value represents the minimum spacing between items in the same column. This spacing is used to compute how many items can fit in a single line, but after the number of items is determined, the actual spacing may possibly be adjusted upward.
But, I'm wondering, what would happen if you override viewDidLayoutSubview(), and call collectionView.collectionViewLayout?.invalidateLayout(); collectionView.collectionViewLayout?.prepareLayout().

UICollectionViewCell sizeForItemAt values being reverted to fitItemSize somehow?

I have set up 2 collectionView in a viewcontroller, both get their data from an endpoint and then reloadData().
One collectionView act like an header tab and have its cell size depend on its intritic size and rely on insetForSection to position/align the cell in the center of the collectionView.
Another have "sort-of" fixed size for themselves where the first cell will be almost the entire width of the collectionView and then the cells after the first one will occupy semi-half the collectionView width.
I have setted-up delegate and extension methods, however for some reason the sizeforItem that focus on the second CollectionView doesn't "stick", they get reverted. When i do :
self.statusOptionCollectionView.reloadData()
self.statusOptionCollectionView.performBatchUpdates({
self.statusOptionCollectionView.layoutIfNeeded()
}) { (complete) in
debugPrint("Batch Update complete")
}
I saw a brief frame of my desired outcome but then the collectionView suddenly undo my sizeForItem code and change the cell to something akin to "size-to-fit". (Pics: Below).
Question is how do i fix this? What is causing this? Is it because i have 2 collectionView in one viewcontroller? I've tried to invalidatingLayout in viewdidlayoutsubviews but it doesn't work. "I did use storyboard but i already delete the collectionView and re-add it, didn't fix it)
I want Something Like This (Focus one the second viewcontroller layout) :
My CollectionView Layout Code is Like This (kindTabCollectionView is the "header", with center alignment) :
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
if collectionView.isEqual(self.kindTabCollectionView){
let layout = collectionViewLayout as! UICollectionViewFlowLayout
let totalCellWidth = layout.itemSize.width * CGFloat(self.kindArray.count)
let totalSpacingWidth = CGFloat(8 * (self.kindArray.count - 1))
let leftInset = (collectionView.bounds.size.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
let rightInset = leftInset
return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
}else{
return UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if collectionView.isEqual(self.kindTabCollectionView){
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
return flowLayout.itemSize
}else{
let height = CGFloat(40.0)
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let widthMargin = (flowLayout.sectionInset.left + flowLayout.sectionInset.left + flowLayout.minimumInteritemSpacing)
if indexPath.item == 0 && indexPath.section == 0{
let width = floor(collectionView.frame.size.width - widthMargin)
return CGSize(width: width, height: height)
}else{
let width = floor((collectionView.frame.size.width / 2) - widthMargin)
return CGSize(width: width, height: height)
}
}
}
However, the result that come out is this :
(Sorry, it was just a few frame, i tried my best to screen shot it, but it did tried to change to correct frame size, but then it just revert to the small "fitToSize" pic)
Check collectionView's "Estimated Size" attribute in the Size Inspector (Storyboard). It should be set to "None" when using an extension of UICollectionViewDelegateFlowLayout to set cell's size.
As stated in the Xcode 11 Release Notes:
Cells in a UICollectionView can now self size with Auto Layout
constrained views in the canvas. To opt into the behavior for existing
collection views, enable “Automatic” for the collection view’s
estimated size, and “Automatic” for cell’s size from the Size
inspector. If deploying before iOS 13, you can activate self sizing
collection view cells by calling performBatchUpdates(_:completion:)
during viewDidLoad(). (45617083)
So, newly created collectionViews have the attribute "Estimated Size" set as "Automatic" and the cell's size is computed considering its subview dimensions, thus ignoring the UICollectionViewDelegateFlowLayout extension methods, even though they are called.

Swift auto layout in UICollectionView with intrinsic height of cells

I'm having a hard time with auto layout and UICollectionView. I have a vertical UICollectionVIewFlowLayout that contains two different types of cells, as in the image below:
The SingleCard cell contains a subview with an image (UIImageView). The HorizList cell contains a subview which is a horizontal UICollectionView (a list of thumbnail photos). The heights of the two cell types are as follows:
SingleCard: the width of the cell shall be the same as the screen width. The height shall be dynamic to the height of the UIImageView (to keep proportions of the image and have it fill screen width no matter of device size)
HorizList: the height is constant, say 200 points.
My attempt, in the CollectionViewController is to do
override func viewDidLoad() {
...
let layout = collectionView?.collectionViewLayout as! UICollectionViewFlowLayout
layout.estimatedItemSize = CGSize(width: 200.0, height: 235.0)
}
This I do to get the two cell's intrinsic size to apply. Then I do not implement
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
at all.
My question is, what do I implement in
override var intrinsicContentSize: CGSize {
??
}
for the two different cell types? Maybe this is not the only thing I need to do, if so I'm grateful for additional hints.
Thanks!
/ola

Dynamic height for custom collection view cell by using autolayouts

I had created a custom collection view cell. It has a UILabel, UIView, UIButton and then UITextView.
Requirements are:
UILabel's text size can be anything. Based on that it's content size label must re-size. It is working properly.
any number of views can be added to UIView. Based on that this UIView must be re-size.
UIButton is just a normal button. when we click on it, the below TextView will toggle.
The constraints that I applied are:
use this method and calculate the size that is appropriate for you and return it.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let Labell : UILabel = UILabel()
Labell.text = self.items[indexPath.item]
let labelTextHeight = Labell.intrinsicContentSize.height
//calculate size for UIView, UIButton, TextView and then add it
return CGSize(width: /*fix width*/, height: labelTextHeigh + /*your UIView height and other height*/)
}
don't forget to extend your viewController from UICollectionViewDelegateFlowLayout

Auto Layout size of CollectionViewCell in relation to CollectionView

I have a CollectionViewCell placed in a CollectionView. The size of the CollectionView is set via Auto Layout and can change at times.
I want to (programmatically) set constraints on my CollectionView, so that the size of the CollectionViewCell is responsive to the size of the CollectionView (Say the width of the Cell is equal to the width of the CollectionView-100 and the height should be equal).
How can I declare constraints between the Cell and the CollectionView?
When, where and how do set those constraints? Do I have to call setNeedsLayout on the Cells when the size of the CollectionView changes?
I searched for this quite long and all I found was always about changing the size of the Cell according to its content – that’s not what I am trying to do. The only hint that I got is that I might have to subclass CollectionViewLayout – but I’m not sure about that.
Any help is appreciated!
You cannot achieve this using constraints, but you can easily do this using UICollectionViewDelegate's sizeForItemAtIndexPath method. Example:
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: collectionView.frame.size.width / 2, height: collectionView.frame.size.height/ 2)
}
And after changing the collectionView size all you need to do is call its reloadData method.
I can suggest for you 2 ways how to do this:
In your UIViewController try to set the collectionViewLayout property in viewDidLayoutSubviews method. I'm not sure if it works when screen orientation will change.
- (void)viewDidLayoutSubviews
{
UICollectionViewFlowLayout *flow = YOUR_COLLECTION_VIEW.collectionViewLayout;
float width = CGRectGetWidth(YOUR_COLLECTION_VIEW.frame)-100.f;
float height = CGRectGetHeight(YOUR_COLLECTION_VIEW.frame);
YOUR_COLLECTION_VIEW.collectionViewLayout = flow;
}
If that doesn't work, then I'm 100% sure that it will work if you subclass the collectionView and insert the same code but in (void)layoutSubviews
Set the cell height and get the collection view height in UICollectionViewDelegateFlowLayout sizeForItemAt method. You can set the cell size based on a whole number or multiply as a percentage if that's more appropriate.
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout, sizeForItemAt
indexPath: IndexPath) -> CGSize {
let heigh = collectionView.frame.height
let widthOfCell = collectionView.frame.width - 100 // OP request
return CGSize(width: width, height: height)
}

Resources