How do I set a different height for each UICollectionViewCell - ios

I feel like this should be a lot easier than it is. All I want to do is have a different height for each of my collection view's cells (depending on the size of the label inside each cell). I'm using sizeForItemAtIndexPath, but the trouble is figuring out the height before the cell is created.
What I have now:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
// target width of each cell - widht of the collectionView
let targetWidth: CGFloat = collectionView.frame.width - 20.0
// setup a prototype cell
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCustomCellIdentifier", forIndexPath: indexPath) as! MyCustomCell
// for the sake of simplicity, let's just assume data is coming from somewhere else
cell.nameLabel.text = data.name
cell.notesLabel.text = data.notes
// resize - layoutSubviews in LocationCell controller
cell.layoutIfNeeded()
// get the size based on constraints
var size = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
// force width
size.width = targetWidth
return size
}
What's not working is dequeueReusableCellWithReuseIdentifier. I'm guessing it's because the UICollectionViewCell is not yet available? I also tried registerClass to get that, but that doesn't seem to work either. :(
Is there an easier way to do this entirely? All I need to do is figure out what the height is for the cell before it's created. I need an instance of the UICollectionViewCell subclass in order to even be able to start (so I can actually access the label and try to determine a height). Been stuck on this for hours. :/

Use this method
func heightForComment(comment:NSString,font: UIFont, width: CGFloat) -> CGFloat {
let rect = NSString(string: comment).boundingRectWithSize(CGSize(width: width, height: CGFloat(MAXFLOAT)), options: .UsesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return ceil(rect.height)
}

This is not the proper way to do this, but works for me
use this function to obtain the size of the text
func labelSize(texto: NSString) -> CGRect {
var atributos = [NSFontAttributeName: UIFont.systemFontOfSize(17)]
var labelSize = texto.boundingRectWithSize(CGSizeMake(280, CGFloat(MAXFLOAT)), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: atributos, context: nil)
return labelSize
}

Related

Get dynamic height for UICollectionViewCell with custom layout

I need to create an UICollectionView with variable cell height but keeping a fixed vertical gap between cells. Something like this
I found a lot of examples, tutorials and libraries out there but all these need to implement a delegate method providing the height for each item.
My cells have a number of labels which can have one or more lines so I don't know in advance the final height of an element after the cell layouts its views.
Is there any way to achieve what I need?
You can calculate your cell height with systemLayoutSizeFitting function.
https://developer.apple.com/documentation/uikit/uiview/1622623-systemlayoutsizefitting
For example: (UICollectionViewCell is a subclass of UICollectionReusableView so you can use this for cell and header/footer too)
public extension UICollectionReusableView {
static func autoResizingView<T: UICollectionReusableView>(type: T.Type) -> T {
let nibViews = Bundle.main.loadNibNamed(T.identifier, owner: nil, options: nil)
return nibViews?.first as? T ?? T()
}
static func autoLayoutSize<T: UICollectionReusableView>(type: T.Type, targetWidth: CGFloat, configure: ((T) -> Void)?) -> CGSize {
let resizingView = UICollectionReusableView.autoResizingView(type: type)
resizingView.prepareForReuse()
configure?(resizingView)
resizingView.setNeedsLayout()
resizingView.layoutIfNeeded()
let targetSize = CGSize(width: targetWidth, height: 0)
let calculateView: UIView
if let contentView = (resizingView as? UICollectionViewCell)?.contentView {
calculateView = contentView
} else {
calculateView = resizingView
}
// Calculate the size (height) using Auto Layout
let autoLayoutSize = calculateView.systemLayoutSizeFitting(
targetSize,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .defaultLow)
return autoLayoutSize
}
}

AutoSizing cells: cell width equal to the CollectionView

I'm using AutoSizing cells with Autolayout and UICollectionView.
I can specify constraints in code on cell initialization:
func configureCell() {
snp.makeConstraints { (make) in
make.width.equalToSuperview()
}
}
However, the app crashes as the cell hasn't been yet added to the collectionView.
Questions
At which stage of the cell's lifecycle it is possible to add a
constraint with cell's width?
Is there any default way of making a cell'swidthequal to the
widthof thecollectionViewwithout accessing an instance of
UIScreenorUIWindow`?
Edit
The question is not duplicate, as it is not about how to use the AutoSizing cells feature, but at which stage of the cell lifecycle to apply constraints to achieve the desired result when working with AutoLayout.
To implement self-sizing collection view cells you need to do two things:
Specify estimatedItemSize on UICollectionViewFlowLayout
Implement preferredLayoutAttributesFitting(_:) on your cell
1. Specifying estimatedItemSize on UICollectionViewFlowLayout
The default value of this property is CGSizeZero. Setting it to any other value causes the collection view to query each cell for its actual size using the cell’s preferredLayoutAttributesFitting(_:) method. If all of your cells are the same height, use the itemSize property, instead of this property, to specify the cell size instead.
This is just an estimate which is used to calculate the content size of the scroll view, set it to something sensible.
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.estimatedItemSize = CGSize(width: collectionView.frame.width, height: 100)
2. Implement preferredLayoutAttributesFitting(_:) on your UICollectionViewCell subclass
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
// Specify you want _full width_
let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
// Calculate the size (height) using Auto Layout
let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.defaultLow)
let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
// Assign the new size to the layout attributes
autoLayoutAttributes.frame = autoLayoutFrame
return autoLayoutAttributes
}
You'll need to implement sizeForItemAt: to calculate the size.
We've also used a "sizing cell" if your cells have variable height. Eg:
class MyFancyCell: UICollectionViewCell {
class func cellSize(_ content: SomeContent, withWidth width: CGFloat) -> CGSize {
sizingCell.content = content
sizingCell.updateCellLayout(width)
return sizingCell.systemLayoutSizeFitting(UILayoutFittingExpandedSize)
}
fileprivate static let sizingCell = Bundle.main.loadNibNamed("ContentCell", owner: nil, options: nil)!.first as! ContentCell
func updateCellLayout(width: CGFloat) {
//Set constraints and calculate size
}
}

DecorationView gets resized when AutoSizing cells are enabled in UICollectionViewFlowLayout

Environment:
UICollectionView that looks like UITableView
Custom UICollectionViewFlowLayout subclass to define the frame of the DecorationView
Self-Sizing cells enabled
Expected behavior:
A DecorationView that should be placed as a background for every section of the UICollectionView
Observed Behavior:
The DecorationView collapses to an arbitrary size:
Seems that UICollectionView tries to calculate an automatic size for the DecorationView. If I disable Self-Sizing cells, the decoration view is being placed exactly at the expected place.
Is there any way to disable Self-Sizing for DecorationView ?
In my UICollectionViewFlowLayout subclass I simply take the first and last cells in the section and stretch the background to fill the space underneath them. The problem is that UICollectionView does not respect the size calculated there:
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let collectionView = collectionView else {
return nil
}
let section = indexPath.section
let attrs = UICollectionViewLayoutAttributes(forDecorationViewOfKind: backgroundViewClass.reuseIdentifier(),
with: indexPath)
let numberOfItems = collectionView.numberOfItems(inSection: section)
let lastIndex = numberOfItems - 1
guard let firstItemAttributes = layoutAttributesForItem(at: IndexPath(indexes: [section, 0])),
let lastItemAttributes = layoutAttributesForItem(at: IndexPath(indexes: [section, lastIndex])) else {
return nil
}
let startFrame = firstItemAttributes.frame
let endFrame = lastItemAttributes.frame
let origin = startFrame.origin
let size = CGSize(width: startFrame.width,
height: -startFrame.minY + endFrame.maxY)
let frame = CGRect(origin: origin, size: size)
attrs.frame = frame
attrs.zIndex = -1
return attrs
}
It's possible that the frames of your decoration views are not being updated (i.e. invalidated) after the frames of your cells have been self-sized. The result is that the width of each decoration view remains at its default size.
Try implementing this function, which should invalidate the layout of the decoration view for each section every time the layout of an item in that section is invalidated:
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
let invalidatedSections = context.invalidatedItemIndexPaths?.map { $0.section } ?? []
let decorationIndexPaths = invalidatedSections.map { IndexPath(item: 0, section: $0) }
context.invalidateDecorationElements(ofKind: backgroundViewClass.reuseIdentifier(), at: decorationIndexPaths)
super.invalidateLayout(with: context)
}

How to change UICollectionView Cell width base on data

I have a UICollectionView with a cell only contain of one label. I've implemented it working fine, but some string values set to label inside cell cut out. Check below image.
Select cell should display as "Day Before Yesterday". If there's a way to adjust cell width base on data length I can fix this. is it possible ?
PS: some similar questions suggested below method. so I've tried it but no luck.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if(collectionView == dateRangeCollectionView){
return (dateRanges[indexPath.item] as NSString).size(attributes: nil)
}else{
return (OrderStatus[indexPath.item] as NSString).size(attributes: nil)
}
}
I don't think this method even exist in swift 4. when I start to type "sizeForItemAt" Xcode didn't suggest this method.
Please use Self Sizing Cells. Follow the below steps for more informaton
Self sizing cells are only supported with flow layout so make sure thats what you are using.
Follow the following steps :
Set estimatedItemSize on UICollectionViewFlowLayout
Add support for sizing on your cell subclass (Autolayout). More info here.
Checkout this github repo : SelfSizingCollectionViewCell
To calculate the size of each cell, you will need to figure out the width that the text would take, also accounting for the padding given. Use this to get the width of the cell, and return the value in the sizeForItem delegate.
func labelSizeWithString(text: String,fontSize: CGFloat, maxWidth : CGFloat) -> CGRect{
let font = UIFont.systemFontOfSize(fontSize)
let label = UILabel(frame: CGRectMake(0, 0, maxWidth, CGFloat.max))
label.numberOfLines = 0
label.font = font
label.text = text
label.sizeToFit()
return label.frame
}
If you want the text to appear in one or two lines, use the option for numberOfLines.
For the width you get, add in the appropriate padding value.

UICollectionView challenge: set height on the last cell to fit availabe space

Can you solve the following problem: I have a UICollectionView with a fixed number of CollectionView Cells - but the hight of the devices change and I have to dynamically calculate the last(buttom) cell.
------
I: Cell 1
------
I: Cell 2
------
I: Cell 3, This cell has a dynamic hight
I
------
I have tried the following:
note: let view = self.cells[indexPath.row] is a fixed list of UIView that I add to the UICollectionView contextView in cellForItemAt
Also the code is in a class that implement UICollectionViewDelegateFlowLayout
self.collectionView?.isScrollEnabled = false
var totalHight: CGFloat = 0
public func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let view = self.cells[indexPath.row]
self.totalHight = self.totalHight + view.frame.size.height
if (self.totalHight > self.frame.size.height){
let diff = self.totalHight - self.frame.size.height
let height = (view.frame.size.height - diff)
view.frame.size = CGSize(width: self.frame.size.width,
height: height)
self.totalHight = 0
return view.frame.size
}else{
return CGSize(width: self.frame.size.width, height: view.frame.size.height)
}
}
I can't tell if that code you've tried is in your view controller or a custom layout. What was the result?
This can be done with a custom UICollectionViewLayout instance. You can probably get by with subclassing UICollectionViewFlowLayout and overriding
collectionView:layout:sizeForItemAtIndexPath:
Then you can look at the remaining space on the screen. It will probably pose problems if the content ends up exceeding the available space.
One approach you might look at is getting the layout attributes for the previous cell, and that will give you its frame, something like this:
let previousIndexPath = IndexPath(row: indexPath.row - 1, section: indexPath.section)
let attribs = layoutAttributesForItem(at: previousIndexPath)
let previousFrame = attribs.frame
let availableHeight = totalHeight - previousFrame.size.height
return CGSize(width: previousFrame.width, height: availableHeight)
See more here:
https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html
https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617797-layoutattributesforitem
Edit: as an aside, it sounds like maybe using UIStackView and autolayout constraints would make this thing much easier, since your content is static and doesn't scroll.

Resources