I have a collection view with a section header that displays a users information. The header can include a bio from the user. I am having trouble with resizing the header if the bio is long. The label will not display the whole bio because the header stays at a fixed height.
I created the collection view via storyboard however I added the constraints programmatically.
Here is the bio constraints, I thought by setting the height and lines to 0 I would be okay,
addSubview(bioLabel)
bioLabel.anchor(top: editProfileButton.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, paddingTop: 12, paddingLeft: 12, paddingBottom: 0, paddingRight: 12, width: 0, height: 0)
I also thought I could override the storyboard by
// Trying to override storyboard header height so the header can strech depending on the bio height
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
//TODO change height to be bounds.height of bioLabel?
return .init(width: view.frame.width, height: 300)
}
but it appears even if I try to change the size through code, it keeps the storyboard height of 225 ( which is what I would like)
None of the answers given so far are good practices since calculating the size manually or by rendering a label offscreen are both inefficient and violate the single source of truth principle.
Instances of UICollectionReusableView have the preferredLayoutAttributesFitting() method. When the cell becomes displayed, this method will be called. In this method, if you use Auto Layout, you ask the whole cell for its systemLayoutSizeFitting() and then modify the attributes. The layout will then be responsible to apply them via layout invalidation.
Use an instance of UICollectionReusableView to be your header.
And calculate the actual size of header in this method
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { }
Make sure you header is ready for dynamic height as well. Good luck.
You can get Label Height using below function
func heightForLabel(text: String, font: UIFont, width: CGFloat) -> CGFloat {
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
}
Collection Header Method
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let height = self.heightForLabel(text: 'your text', font: 'font which you in label' , width: 'your label width')
return .init(width: view.frame.width, height: height+10) //10 is extra height if you want or you can remove it.
}
This function will do the work. Just pass the label text
func estimatedLabelHeight(text: String) -> CGFloat {
let size = CGSize(width: view.frame.width - 16, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 10)]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
And use it like
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
//TODO change height to be bounds.height of bioLabel?
return .init(width: view.frame.width, height: estimatedLabelHeight(text: "Text that your label is gonna show"))
}
You need to calculate the size of cell height in method
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
var size = CGSize()
let cell = YourUICollectionViewCell()
cell.textLabel?.text = data?[indexPath.row].text
let fitting = CGSize(width: cell.frame.size.width, height: 1)
size = cell.contentView.systemLayoutSizeFitting(fitting,
withHorizontalFittingPriority: .required,
verticalFittingPriority: UILayoutPriority(1))
return size.height //or just return size
}
If UILabel is custom then you need to set the bottom Constraint of UILabel to view with less than 1000 priority.
Hope it helps
Related
I am working on a project where we use a UICollectionView with a custom cell. The cell looks like this. All of my elements are contained in a UIView.
Of course, the text is much shorter.
Anyway, I decide the width of the CollectionView in the sizeForItemAt function:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width - 50, height: (collectionView.frame.width * 0.7))
}
I need to find a way to keep this fixed width but to make the height of the cell resizable when dynamic type is enabled. The labels at the bottom of the cell must be able to grow in size and therefore resize the cell at the bottom as much as they need without affecting the ImageView's size at the top. However, this seems harder than it looks since my ImageView has a size of 0.7 * the width of my CollectionView (so it has a proper height in any screen).
My labels are contained in a VerticalStackView and my VerticalStackView and the "1" label are contained in a HorizontalStackView.
I have tried to use dynamic type on this kind of view hierarchy in a sample project and the view grows in height without affecting the ImageView but I cannot seem to do the same in a CollectionViewCell.
I tried using auto-layout with Automatic Size enabled for my CollectionViewCell but that just ruins both my width and height and also clips the labels that I cannot see them anymore.
I hope you can help me!
Thank you so much.
You can find your height of the label using text as:
extension String {
func height(withWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(
with: constraintRect,
options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: font],
context: nil)
return ceil(boundingBox.height)
}
}
Modify your collection view's sizeForItemAt as:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width - 50.0
let labelHeight = "your text".height(withWidth: width, font: .systemFont(ofSize: 17.0))
let imageHeight = width * 0.7
return CGSize(width: width, height: labelHeight+imageHeight)
}
My scenario, I am trying to Implement UICollectionView horizontal scrollview first and last cell center alignment based on selection. Here, Very first time CollectionViewCell first cell not showing exact center position. If I didselect once everything working fine.
I need to fix very first time app open first cell not showing exact center position.
NOTE: I didn’t added header and footer also I am done my designing using storyboard.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let text = self.items[indexPath.row]
let cellWidth = text.size(withAttributes:[.font: UIFont.systemFont(ofSize: 14.0)]).width + 10.0
return CGSize(width: cellWidth, height: collectionView.bounds.height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let inset: CGFloat = collectionView.frame.width * 0.5 - 52 * 0.5
return UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
}
You cannot use 52 directly since as the cell width, since the width of each cell is dynamic.
You need to calculate the leftInsets and rightInsets of the collectionView based on the actual width of first and last cell.
You can calculate the width of first and last cell the same way you did in collectionView(_:layout:sizeForItemAt) method using the first and last element of items array, i.e.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
//Calculate width of first cell
var firstCellWidth: CGFloat = 52.0
if let textAtFirstIndex = self.items.first {
firstCellWidth = textAtFirstIndex.size(withAttributes:[.font: UIFont.systemFont(ofSize: 14.0)]).width + 10.0
}
//Calculate width of last cell
var lastCellWidth: CGFloat = 52.0
if let textAtLastIndex = self.items.last {
lastCellWidth = textAtLastIndex.size(withAttributes:[.font: UIFont.systemFont(ofSize: 14.0)]).width + 10.0
}
//Calculate leftInset and rightInset
let leftInset: CGFloat = (collectionView.frame.width * 0.5) - (firstCellWidth * 0.5)
let rightInset: CGFloat = (collectionView.frame.width * 0.5) - (lastCellWidth * 0.5)
return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
}
Example:
Let me know in case you still face any issues.
I am trying to build collectionview with textview inside collectionviewcell.I want to calculate height of cell based on textview.
I use sizeThatFits method to calculate the height but as the text grow bigger, the height of collectionviewcell does not match with text.
I tried to use boundingRect but the result was also incorrect
Is their anyway to calculate correct height of textview? or how to solve this problem.
Thank you!!
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let textview = UITextView()
textview.text = statusText[indexPath.row]
let actualsize = textview.sizeThatFits(CGSize(width: collectionView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
return CGSize(width: collectionView.frame.size.width, height: actualsize.height)
}
By default, when you create a new instance of UITextView the font attribute is nil, you must associate a font to UITextView before use sizeThatFit.
If you are using the default font, just add textview.font = UIFont.systemFont(ofSize: 14)
Change your function like this :
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let textview = UITextView()
textview.text = statusText[indexPath.row]
textview.font = UIFont.systemFont(ofSize: 14)
let actualsize = textview.sizeThatFits(CGSize(width: collectionView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
return CGSize(width: collectionView.frame.size.width, height: actualsize.height)
}
I have a very simple demo which use UICollectionView to show a list of pictures, I use a customize cell class which contains an UIImage and a Label as below, there is no constraint.
Then I set the size of the cell in code as below. there are 3 columns on each row. all margins are set to 10.
// Calculate size of cell
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let totalSpace = marginLeft + marginRight + (colomnSpacing * CGFloat(numberOfItemsPerRow - 1))
let size = Int((collectionView.bounds.width - totalSpace) / CGFloat(numberOfItemsPerRow))
// 20 is for label height.
return CGSize(width: size, height: size + 20)
}
// Section margin, if you only have one section, then this is the CollectionView's margin.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: marginTop, left: marginLeft, bottom: marginBottom, right: marginRight) // top, left, bottom, right.
}
// Set row spacing
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return rowSpacing
}
// Set column spacing
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return colomnSpacing
}
Here is the code used to set the image and label size. the image's width takes 1/3 width of the screen(exclude the column space), and the height equals to width. the label width was equal to image width, label's height was set to 20.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! SearchImageCell
// Set image size
cell.imageView.frame = CGRect(x: 0, y: 0, width: cell.frame.width, height: cell.frame.height - 20)
cell.imageView.contentMode = .ScaleAspectFill
let currentItem = self.responseArray[indexPath.row]
let imageUrl = currentItem["imgurl"] as? String
let url = NSURL(string: imageUrl!)
// Load the image
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
let data = NSData(contentsOfURL: url!)
dispatch_async(dispatch_get_main_queue(), {
cell.imageView.image = UIImage(data: data!)
})
}
// Set label size
cell.imageDesc.frame = CGRect(x: 0, y: cell.imageView.frame.height, width: cell.frame.width, height: 20)
let shujia = currentItem["shujia"] as? String
cell.imageDesc.text = shujia!
cell.imageDesc.font = cell.imageDesc.font.fontWithSize(14)
cell.imageDesc.textAlignment = .Center
return cell
}
The question is:
When I load the image first, the size of the image was incorrect, like below, you can see the image's height occupy entire height of the cell, and the label was not show up.
But, when I scroll down and up(This makes the cell being reused), the image was resized, and I got the correct result, this is what I want!
I tried to add some debug code to print the size of the cell/image/label, it seems all size was correct.
cell width: 100.0
cell height: 120.0
image width: 100.0
image height: 100.0
label width: 100.0
label height: 20.0
So, what's wrong?
There are some similar question on SO(here, here), but the answers not works for me, like
1. cell.layoutIfNeeded()
2. self.collectionViewLayout.invalidateLayout()
3. self.collectionView?.layoutIfNeeded()
Please help, let me know if you need more code.
set translatesAutoresizingMaskIntoConstraints to true for your image view and label. maybe it works.
Notes: when use IB it will set them to false
I am trying to add padding to the container of a UICollectionView. I would like it to appear as such that there is a 10pt padding all around. So in the example screen, there is a 10pt padding on the bottom from:
layout.minimumInteritemSpacing = layout.minimumLineSpacing = 10;
I am using a UICollectionViewFlowLayout to layout the cells. I have also tried a "trick" where I add a 10pt view on top, but the content doesn't appear to scroll through the view since they are separate.
Please try to put following code inside viewDidLoad():
collectionView!.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
This will add padding to every side of the collectionView.
PS: I know the question is here for a while but it might help somebody ;)
It seems like the sectionInset property of your UICollectionViewFlowLayout is what you need to modify.
You can use a UIEdgeInsetsMake to create a UIEdgeInsets with margins for top, left, right, and bottom, and set this to the sectionInset property.
Here's the documentation for UICollectionViewFlowLayout: https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewFlowLayout_class/Reference/Reference.html
I used a couple of methods from the UICollectionViewDelegateFlowLayout delegate to accomplish what I was looking for. I was attempting to add 'padding' to cells that had a shadow. This was to ensure they had the effect of 'floating,' and were not touching the edges of the UICollectionView.
First, call the sizeForItemAt indexPath method and give the cells a size:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.bounds.width * 0.80, height: collectionView.bounds.height * 0.98)
}
Then, call the insetForSectionAt section method, and set the insets for the cells:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 20, left: 25, bottom: 20, right: 25)
}
If you're using configurations, set the contentInsetsReference on the layout's configuration to follow layoutMargins, then set layoutMargins on the collectionView itself.
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
config.headerMode = .supplementary
let layout = UICollectionViewCompositionalLayout.list(using: config)
layout.configuration.contentInsetsReference = .layoutMargins
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.layoutMargins = .init(top: 16, left: 64, bottom: 16, right: 64)
In case you're using a UICollectionLayoutListConfiguration, it seems that to get smaller margins with certain appearances, you've to do that on section level like this:
private func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
// Create a list configuration as usual
var config = UICollectionLayoutListConfiguration(appearance: .sidebarPlain)
config.headerMode = .supplementary
// Change the section's contentInsets to adjust the side margins
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
section.contentInsets.leading = 10
section.contentInsets.trailing = 10
return section
}
return layout
}
Based on feedback from Apple (https://developer.apple.com/forums/thread/679863)