I have a bit of a special scenario and no matter what StackOverflow answer I find, it doesn't seem to work for me. I have a collection view with a dynamic number of cells (anywhere between 2-3 cells). However, the height of this collection view will always be the same, no matter how many cells there are.
Here is the logic I have for setting the cell heights:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// The cell's width and height, to be calculated
var cellWidth: CGFloat = 0.0, cellHeight: CGFloat = 0.0
// The number of cells to show, 2-3
let count = numberOfCellsToShow
// The width of the collection view
let collectionViewWidth = collectionView.bounds.size.width
// Calculate the maximum height for collection view (16:9 ratio)
let collectionViewMaxHeight = collectionViewWidth / (16 / 9)
// Get the width and height of each cell, with 4dp of spacing between cells
if (count == 2) {
cellWidth = (collectionViewWidth - CGFloat(4)) / 2
cellHeight = collectionViewMaxHeight
} else if (count == 3) {
cellWidth = (collectionViewWidth - (CGFloat(4) * 2)) / 3
cellHeight = collectionViewMaxHeight
}
return CGSize(width: cellWidth, height: cellHeight)
}
Now, I want to make sure that the height of the collection view fits the content, but this is where I'm struggling to make it happen.
My collection view is inside a stack view, with all constraints set to the edges of the stack view.
What do I need to do to set the height of my collection view to fit the contents?
Related
trying to calculate height of a cell with specified width and cannot make it right. Here is a snippet. There are two columns specified by the custom layout which knows the column width.
let cell = TextNoteCell2.loadFromNib()
var frame = cell.frame
frame.size.width = columnWidth // 187.5
frame.size.height = 0 // it does not work either without this line.
cell.frame = frame
cell.update(text: note.text)
cell.contentView.layoutIfNeeded()
let size = cell.contentView.systemLayoutSizeFitting(CGSize(width: columnWidth, height: 0)) // 251.5 x 52.5
print(cell) // 187.5 x 0
return size.height
Both size and cell.frame are incorrect.
Cell has a text label inside with 16px margins on each label edge.
Thank you in advance.
To calculate the size for a UILabel to fully display the given text, i would add a helper as below,
extension UILabel {
public static func estimatedSize(_ text: String, targetSize: CGSize = .zero) -> CGSize {
let label = UILabel(frame: .zero)
label.numberOfLines = 0
label.text = text
return label.sizeThatFits(targetSize)
}
}
Now that you know how much size is required for your text, you can calculate the cell size by adding the margins you specified in the cell i.e 16.0 on each side so, the calculation should be as below,
let intrinsicMargin: CGFloat = 16.0 + 16.0
let targetWidth: CGFloat = 187.0 - intrinsicMargin
let labelSize = UILabel.estimatedSize(note.text, targetSize: CGSize(width: targetWidth, height: 0))
let cellSize = CGSize(width: labelSize.width + intrinsicMargin, height: labelSize.height + intrinsicMargin)
Hope you will get the required results. One more improvement would be to calculate the width based on the screen size and number of columns instead of hard coded 187.0
That cell you are loading from a nib has no view to be placed in, so it has an incorrect frame.
You need to either manually add it to a view, then measure it, or you'll need to dequeu it from the collectionView so it's already within a container view
For Swift 4.2 updated answer is to handle height and width of uicollectionview Cell on the basis of uilabel text
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let size = (self.FILTERTitles[indexPath.row] as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14.0)])
return CGSize(width: size.width + 38.0, height: size.height + 25.0)
}
Note, I have scoured the internet and have not found a place to both size and centers cells that works. I tried doing it myself but I keep running to bugs I can't avoid. I am new to Swift. My code:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath:IndexPath) -> CGSize {
let cellWidth = collectionView.frame.size.width / 7.0
let cellHeight = collectionView.frame.height - 4.0
let imageSideLength = cellWidth < cellHeight ? cellWidth : cellHeight
return CGSize(width: imageSideLength, height: imageSideLength)
}
//centers the cells
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
// Make sure that the number of items is worth the computing effort.
guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout,
let dataSourceCount = photoCollectionView.dataSource?.collectionView(photoCollectionView, numberOfItemsInSection: section),
dataSourceCount > 0 else {
return .zero
}
let cellCount = CGFloat(dataSourceCount)
let itemSpacing = flowLayout.minimumInteritemSpacing
let cellWidth = flowLayout.itemSize.width + itemSpacing
let cellHeight = flowLayout.itemSize.height
var insets = flowLayout.sectionInset
// Make sure to remove the last item spacing or it will
// miscalculate the actual total width.
let totalCellWidth = (cellWidth * cellCount) - itemSpacing
let contentWidth = collectionView.frame.size.width - collectionView.contentInset.left - collectionView.contentInset.right
let contentHeight = collectionView.frame.size.height
// If the number of cells that exist take up less room than the
// collection view width, then center the content with the appropriate insets.
// Otherwise return the default layout inset.
guard totalCellWidth < contentWidth else {
return insets
}
// Calculate the right amount of padding to center the cells.
let padding = (contentWidth - totalCellWidth) / 2.0
insets.left = padding
insets.right = padding
insets.top = (contentHeight - cellHeight) / 2.0
//insets.bottom = (contentHeight - cellHeight) / 2.0
return insets
}
}
I try to use two separate functions: the first to size the cells and the second to center the cells. (Note I only want new cells to expand horizontally, with a maximum of 6 cells.) However, my calculation of cell height and width in the 2nd function does not agree with how I set it in the first function, setting off a chain of issues. Any insight on how to both size and center the cells such that I can have 1-6 cells horizontally fit on my screen centered would be great.
Your layout calls are conflicting. Try following THIS Tutorial to get the hang of it.
Otherwise a good answer for this is HERE
var flowLayout: UICollectionViewFlowLayout {
let _flowLayout = UICollectionViewFlowLayout()
// edit properties here
_flowLayout.itemSize = CGSize(width: 98, height: 134)
_flowLayout.sectionInset = UIEdgeInsetsMake(0, 5, 0, 5)
_flowLayout.scrollDirection = UICollectionViewScrollDirection.horizontal
_flowLayout.minimumInteritemSpacing = 0.0
// edit properties here
return _flowLayout
}
Set it with:
self.collectionView.collectionViewLayout = flowLayout // after initializing it another way
// or
UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
I would like to adjust this function (that was created originally for a collectionView)
let place = places[indexPath.row]
let annotationPadding = CGFloat(5)
let font = UIFont.systemFont(ofSize: 15)
let commentHeight = place.heightForComment(font, width: width)
let height = annotationPadding + commentHeight + annotationPadding
return height
So I can add it in func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { } in my tableView, how can I do? (My problem is that CGFloat do not accept width)
Assuming heightForComment for the collection view takes the width of the collection view cell, you can pass in the width of the table view, since table view cells are as wide as their containing table view:
let commentHeight = place.heightForComment(font, width: tableView.bounds.size.width)
If there is additional leading or trailing margins for the comment in the table view, you can subtract that from the table view width:
let commentHeight = place.heightForComment(font, width: tableView.bounds.size.width - (20 * 2)) // 20 points margin on left and right
Im trying to use estimatedItemSize to dynamically resize collectionView cells to fit they're content tower I'm having a few problems with different device sizes and particularly the width not resizing, its just staying the same size as it is in interface builder.
I have done my Interface Builder stuff on an iPhone 7 so the screen width is 375. In my storyBoard i have the following.
outerCollectionView pinned to all 4 sides of the superView with 0 margin
outerCollectionView Cell of w: 339 h:550
Inside this cell i have:
innerCollectionView pinned to all 4 sides of the cell with 0 margin
innerCollectionView constant w: 339 h: 550 added in IB
Inside my DataSource and Delegate class i have defined my flow layout in the viewDidLoad function:
let width = UIScreen.main.bounds.size.width
let height = UIScreen.main.bounds.size.height
let flow = outerCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
flow.estimatedItemSize = CGSize(width: width - ((width / 18.75)), height: height / 4)
flow.minimumLineSpacing = (width / 37.5)
flow.sectionInset = UIEdgeInsets(top: (width / 37.5), left: (width / 37.5), bottom: (width / 37.5), right: (width / 37.5))
And inside the custom cell class that holds the "inner" collection i set some of these constant constraints and define the flow layout to size the items:
// This is the correct screen size
// let width = innerCollectionView.frame.size.width
let width = UIScreen.main.bounds.size.width
let flow = innerCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
flow.sectionInset = UIEdgeInsetsMake((width / 37.5), (width / 37.5), (width / 37.5), (width / 37.5))
flow.itemSize = CGSize(width: (width / 6 - (width / 37.5)), height: (width / 6 - (width / 37.5)))
flow.minimumInteritemSpacing = (width / 37.5)
flow.minimumLineSpacing = (width / 37.5)
innerCollectionHeight.constant = innerCollectionView.collectionViewLayout.collectionViewContentSize.height
innerCollectionWidth.constant = innerCollectionView.collectionViewLayout.collectionViewContentSize.width
Now all this works fine and the cells resize vertically to fit my content nicely on the iPhone 7 simulator, showing a single column cells correctly populated. But when i step up a device size does not adjust the width of the cell. And when i go down to a smaller device i get a whole bunch of Xcode errors about how the width of the cell is is to big because it hasn't been resized, but nothing displays. (some pics below)
2017-02-16 12:53:49.632 ParseStarterProject-Swift[26279:394906] The behavior of the UICollectionViewFlowLayout is not defined because:
2017-02-16 12:53:49.632 ParseStarterProject-Swift[26279:394906] the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values.
2017-02-16 12:53:49.632 ParseStarterProject-Swift[26279:394906] Please check the values returned by the delegate.
2017-02-16 12:53:49.633 ParseStarterProject-Swift[26279:394906] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7be38920>, and it is attached to <UICollectionView: 0x7db03600; frame = (0 0; 320 504); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x7d61a550>; layer = <CALayer: 0x7be95880>; contentOffset: {0, 0}; contentSize: {320, 20}> collection view layout: <UICollectionViewFlowLayout: 0x7be38920>.
So obviously i cant use
flow.itemSize =
to set the size of the outer cell because i need the estimated size to adjust the vertical length and hug the content. I just cant figure out why the width isnt rising to suit the screen size??
Large sized device - margin too large: CV width 339
Iphone 7 as i built it - margins normal: CV width 339
---- EDIT -----
tried overriding szeForItemAt but there is no change.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = self.view.frame.size.width
let height = self.view.frame.size.height
if collectionView == self.outerCollectionView {
return CGSize(width: width - 100, height: height / 4)
} else {
return CGSize(width: (width / 6 - (width / 75)), height: (width / 6 - (width / 75)))
}
}
---- EDIT ----
I can edit the preferred item seize of the cell. Using the compressed size option for height and setting my own size for width, (width / 18.7) is the width minus left and right inset previously defined
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let attributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes
let width = UIScreen.main.bounds.size.width
let desiredWidth = width - (width / 18.75)
//let desiredWidth = systemLayoutSizeFitting(UILayoutFittingExpandedSize).width
let desiredHeight = systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
attributes.frame.size.width = desiredWidth
attributes.frame.size.height = desiredHeight
return attributes
}
Bu this stuffs up the constraints as shown below in the image, which i think might just be a script i have running to fillet the top corners of UIViews. But its also stuffing up the cells as it cramming more per line.
Smaller screen, similar problem with the cells:
The solution is to implement UICollectionViewDelegateFlowLayout and call sizeForItemAt like this:
extension YourClass: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
//return size needed
return CGSize(width: (width / 6 - (width / 37.5)), height: (width / 6 - (width / 37.5)))
}
At runtime the returned size will be the one giving the item Size.
Note: implementing sizeForItemAt without UICollectionViewDelegateFlowLayout will not succeed
I used a custom flow layout according to this post.
Here is my implementation:
#implementation CustomLayout
-(void)prepareLayout{
[super prepareLayout];
// [self invalidateLayout];
if(self.collectionView){
CGSize newItemSize=self.itemSize;
// Number of items per row
int itemsPerRow=3;
float totalSpacing=self.minimumLineSpacing*(itemsPerRow-1);
newItemSize.width=(self.collectionView.bounds.size.width -totalSpacing)/itemsPerRow;
if(self.itemSize.height>0){
float itemAspectRatio=self.itemSize.width/self.itemSize.height;
newItemSize.height=newItemSize.width/itemAspectRatio;
}
[self setItemSize:newItemSize];
}
}
#end
This is what I've got:
What did I miss? I've come across some other SO posts but no luck so far.
Swift 3.0. Works for both horizontal and vertical scroll directions and variable spacing
Declare number of cells you want per row
let numberOfCellsPerRow: CGFloat = 3
Configure flowLayout to render specified numberOfCellsPerRow
if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
let horizontalSpacing = flowLayout.scrollDirection == .vertical ? flowLayout.minimumInteritemSpacing : flowLayout.minimumLineSpacing
let cellWidth = (view.frame.width - max(0, numberOfCellsPerRow - 1)*horizontalSpacing)/numberOfCellsPerRow
flowLayout.itemSize = CGSize(width: cellWidth, height: cellWidth)
}
//Make use of UICollectionViewDelegateFlowLayout Protocol
class RVC: UICollectionViewController {
//some code
}
extension RVC: UICollectionViewDelegateFlowLayout{
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
var collectionViewSize = collectionView.frame.size
collectionViewSize.width = collectionViewSize.width/3.0 //Display Three elements in a row.
collectionViewSize.height = collectionViewSize.height/4.0
return collectionViewSize
}
For more information go through below link.
UICollectionView Set number of columns
itemSpacing = "Spacing size between cell."
itemsInOneLine = "Number of item which you want to display in single row."
collectionWidth = "Width of your collection view"
let layout:UICollectionViewFlowLayout = UICollectionViewFlowLayout()
let width = (collectionWidth) - itemSpacing * CGFloat(itemsInOneLine - 1)
layout.itemSize = CGSize(width:floor(width/CGFloat(itemsInOneLine)),height:width/CGFloat(itemsInOneLine))
You don't have to do it by coding. You just have to do in xib.as per iPhone 5 or 5s width become 320. and you want to show 3 cells per row so u have to do 320/3 and as per you get whatever answer your cell size is as per result.If any doubt of my answer ask me.