Determine UICollectionView cell height only by autolayout - ios

I need a collection view which displays cells in a grid. So a standard flow layout is fine for me. However, I want to tell how many cells to show per row, while the cell height should be determined by the autolayout constraints that I put on the cell. Here is my cell layout:
It is quite simple - an image view and two labels below it. Now the image view has an aspect ratio constraint (1:1) which means whenever the width is known for the cell the height should automatically be known by the auto layout rules (there are vertical constraints going through: celltop-image-label1-label2-cellbottom).
Now, since I don't know any other good way to tell the collection view to show 2 items per row, I have overridden UICollectionViewDelegateFlowLayout methods:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let availableWidth = collectionView.frame.width - padding
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: widthPerItem)
}
As you can see, since I don't know the item height I return the same thing as the width, hoping that the autolayout will fix it later. I also set the estimatedItemSize in order the whole mechanism to start working.
The results are quite strange - it seems like the collection view doesn't event take into account the width I return there, mostly depending on the label lengths:
I have seen some other answers where people recommend manually calculating the cell size for width, like telling "layout yourself, then measure yourself, then give me your size for this width", and even though it would still run the autolayout rules under the hood, I would like to know if there is a way of doing this without manually messing with the sizes.

You can easily find out the height of your collectionView cell in the storyboard's Size inspector, as shown below:
Now, just pick up this height from here, and pass it to the overridden UICollectionViewDelegateFlowLayout method:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let availableWidth = collectionView.frame.width - padding
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: **114**)
}
And you will get the desired output.

I ended up implementing a trick to move everything to autolayout. I completely removed the delegate method func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize and added a width constraint for my cell content in the interface builder (set the initial value to something, that's not important). Then, I created an outlet for that constraint in the custom cell class:
class MyCell: UICollectionViewCell {
#IBOutlet weak var cellContentWidth: NSLayoutConstraint!
func updateCellWidth(width: CGFloat) {
cellContentWidth.constant = width
}
}
Later, when the cell is created, I update the width constraint to the precalculated value according to the number of cells that I want per row:
private var cellWidth: CGFloat {
let paddingSpace = itemSpacing * (itemsPerRow - 1) + sectionInsets.left + sectionInsets.right
let availableWidth = collectionView.frame.width - paddingSpace
return availableWidth / itemsPerRow
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell
...
cell.updateCellWidth(width: cellWidth)
return cell
}
And this, together with autolayout cell sizing enabled, will lay out the cells correctly.

Related

UICollectionViewCell With 2 column grid and dynamic height

I'm trying to have collection view with 2 column but dynamic height.
I have used Autolayout and given required constraints to the Cell
By this way I can calculate the dynamic height but its column grids fails.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! MenuListCollectionViewCell
cell.frame.size.width = collectionView.frame.width/2
cell.menuList = self.menuList[indexPath.row]
let resizing = cell.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.fittingSizeLevel)
return resizing
}
This is how I want it to look
Not sure what you mean by "its column grid fails".
Anyway, you need to write a custom collection view layout. The default one (UICollectionViewFlowLayout) allows you to change height of the cells by providing the sizeForItemAt, but that won't change the behavior of the layout that will always arrange cells in rows of the same height (the height of the highest cell).
If I understood correctly, you just want the same layout of this raywenderlich tutorial.
Basically:
Create a subclass of UICollectionViewLayout implementing it's methods:
collectionViewContentSize: return width and height of the collection view content
prepare: where you can calculate the sizes of cells and
collectionView content
layoutAttributesForElements: where you
return an array of UICollectionViewLayoutAttributes in the given
rect
layoutAttributesForItem: where you return the same kind
of attributes, this time specific for an item
assign an object of this class to the collection view layout property
you can use this
This code is somehow written that you can change section inset or minimumInteritemSpacing and this calculate and resize with this parameters
you can use this code or download project from Github
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let numberofItem: CGFloat = 2
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let collectionViewWidth = self.collectionView.bounds.width
let extraSpace = (numberofItem - 1) * flowLayout.minimumInteritemSpacing
let inset = flowLayout.sectionInset.right + flowLayout.sectionInset.left
let width = Int((collectionViewWidth - extraSpace - inset) / numberofItem)
return CGSize(width: width, height: width)
}
I was able to achieve your desired outcome by doing the following.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 20
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (self.view.frame.size.width/2 - 5), height: 100)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = customCollectionView.dequeueReusableCell(withReuseIdentifier: "test", for: indexPath)
cell.layer.backgroundColor = UIColor.red.cgColor
return cell
}
Make sure you have implemented all the correct delegates
UICollectionViewDelegate
UICollectionViewDatasource
UICollectionViewDelegateFlowLayout
where you see height: make it your own desired height as you have specified in your question.
KEY: Make sure in your storyboard you have set the estimate size of the collection view to NONE -> otherwise the code will not work as expected

Prevent sizeForItemAt from using the wrong cell size because of reuse for dynamic collectionView cell

I used auto layout to dynamically calculate the size of the collectionView cell. Some cells are using the dimensions from the reused cells when they first scrolled to view port. As I continue to scroll the collectionView, they will be set to the correct value.
In my sizeForItemAt, I have the following:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if let cachedSize = cachedHeightForIndexPath[indexPath] {
return cachedSize
}
if let cell = collectionView.cellForItem(at: indexPath) {
cell.setNeedsLayout()
cell.layoutIfNeeded()
let size = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
cachedHeightForIndexPath[indexPath] = size
print("value is \(size) for indexpath: \(indexPath)")
return size
}
return CGSize(width: ScreenSize.width, height: 0.0)
}
I have a three sessions, with the first section all cell's height theoretically equals to 88, and all the other sections all cell's height equals to 104.
Originally, only the first section is visible. From the console, I can see the height of the cell is set to 88.0 as expected. As I scroll to the remaining sections(the first section will be invisible and the cells will be reused), some cells from second section and third section are using the value 88.0 as the height of the cells when first scrolled to view port instead of 104. As I continue to scroll, the wrong sized cell will be using 104 as the dimension. How do we force all the cells to recalculate the height and don't use the height from old cell.
You have the right idea, but when you measure the cell by its internal constraints by calling systemLayoutSizeFitting, instead of calling systemLayoutSizeFitting on an existing cell (collectionView.cellForItem), you need to arm yourself with a model cell that you configure the same as cellForItem would configure it and measure that.
Here's how I do it (remarkably similar to what you have, with that one difference; also, I store the size in the model):
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let memosize = self.sections[indexPath.section].itemData[indexPath.row].size
if memosize != .zero {
return memosize
}
self.configure(self.modelCell, forIndexPath:indexPath) // in common with cellForItem
var sz = self.modelCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
sz.width = ceil(sz.width); sz.height = ceil(sz.height)
self.sections[indexPath.section].itemData[indexPath.row].size = sz // memoize
return sz
}

Calculating cell width in UICollectionView inside ContainerView

I have a UICollectionView and I have implemented the delegate to calculate the width of my cells for 0 spacing between cells.
It works great on its own, but when I have it inside a container view less than the size of the device, iOS incorrectly works out the spacing between the cells adding a horizontal space I don't want.
I have verified the width I am using to calculate the cell size is correct, so I'm not sure what is causing the problem.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let widthPerItem = view.frame.width / itemsPerRow
return CGSize(width: widthPerItem, height: 60)
}
You are calculating your cell size based upon the size of the view. Since the collectionView doesn't take up the full width of the screen, you are getting the incorrect value. Instead, you should base your calculation on the width of the collectionView itself.
Use the bounds of the collectionView instead of the frame of the view:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let widthPerItem = collectionView.bounds.width / itemsPerRow
return CGSize(width: widthPerItem, height: 60)
}
Ok, the problem was actually related to my uicollectionviewcontroller not correctly sizing to it's parent container view. Changing to a uiviewcontroller with an embedded uicollectionview plus constraints fixed the problem.

Access custom cell from XIB in UICollectionViewController for sizeForItemAtIndexPath

I have spent 3 days of searching and reading. In my IOS app I have a custom Cell for UICollectionView with xib File. In this custom cell I have 3 labels, images and buttons. 1 label from it is in the middle and always gets different sizes because it's multiline.
So I must calculate all items height. It's not the problem but the problem is only with this one multiline label. So I want calculate the size for each cell and get the size for sizeForItemAtIndexPath in UICollectionViewController.
After 3 days I think I cannot access this. Can I do this from the custom cell class? If yes, how? If I can access it in UICollectionViewController, how?
//MARK: - CollectionView Flow Layout
extension PhotosCollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let viewSize = super.view.bounds.size
let spacing: CGFloat = 0.5
let width = (viewSize.width) - spacing - 10
/*
Here I need my cell with label name "Beschreibungstext"
to calculate the hight from the label with help from rectangle.
return CGSize(width: width, height: customhight)
*/
return CGSize(width: width, height: CGFloat(400))
}
}

Resize images automatically in CollectionViewCell in CollectionView with Auto Layout

I have a collectionView with horizontal UICollectionViewFlowLayout.
I am trying to achieve:
If a device orientation is portrait, UIImageView width will be qual to view.width and let the height be calculated automatically (like it usually happens with Auto Layout). And the same for the landscape mode. Example - standard photo app on the Iphone.
Unfortunately i don't see how to achieve it with autoLayout. I set constraints on UIImageView for it to be equal in size to the cell. But looks like the sell itself cannot be pinned to the Parent View.
After reading similar questions looks like cells must be resized programmatically using
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: width, height: height)
}
And here i am stuck because i know the width of the screen but don't know how to calculate the height dynamically.
About image height:
I have my image declared like this:
var pageImages = [UIImage]()
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: ImageDetailViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! ImageDetailViewCell
let curr = indexPath.row
let imgName = data[albumNumber][curr]["image"]
cell.DetailImageView.image = UIImage(named: imgName!)
return cell
}
If you use the proper UIImageView resize setting (say, aspect fit/fill), then you just need to set the cell's height to your collectionView's (you get a pointer to it as one of the ...sizeForItemAtIndexPath... method parameters) height. You also should call the - layoutIfNeeded method on your cell afterwards.
You can use sizeForItemAtIndexPath: to change the size of collection view cell.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var numberOfCellInRow : Int = 3
var padding : Int = 5
var collectionCellWidth : CGFloat = (self.view.frame.size.width/CGFloat(numberOfCellInRow)) - CGFloat(padding)
return CGSize(width: collectionCellWidth , height: collectionCellWidth)
}
You can get the size of cell via :
((UICollectionViewFlowLayout) self.collectionViewName).itemSize.height)
You can get the image size via :
let sizeOfImage = image.size
let height = image.height
If you want to change the height then change it manually by return CGSize(width: collectionCellWidth , height: cellheight)

Resources