UICollectionViewCell dynamic height w/two dynamic labels & auto layout - ios

I have a UICollectionViewCell subclass that's setup with a prototype cell and constraints (every view is connected both vertically and horizontally).
I have two labels that can vary in size, they can be either one or two lines. To that end I have set two height constraints on each label, one with a greater than or equal (16 or 20 depending on the label) and a less than or equal (32 or 40 depending on the label).
The number of lines on the labels are set to 0. (Though I have tried a variety of settings).
Since I've used auto layout and constraints to setup the view I've specified a width on the content view in the cell's awakeFromNib()
override func awakeFromNib() {
NSLayoutConstraint.activate([contentView.widthAnchor.constraint(equalToConstant: 341)])
super.awakeFromNib()
configureBorder()
}
On the collectionView's layout I've specified the automaticDimension constant for estimateItemSize & itemSize
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
flowLayout.itemSize = UICollectionViewFlowLayoutAutomaticSize
}
With the idea of using auto layout I have not implemented a delegate on the collection view.
I am experiencing very inconsistent layout, some instances the cell's will appear as I'd like them to, in others the labels are truncated and in others the labels are extremely narrow and display text over three lines.
I have tried many things, including calling
.sizeToFit()
on each label after it's text property is set, as well as calling
.setNeedsDisplay()
before the cell returns from cellForItemAt
I'm wondering what is the right way to do this with storyboard constraints. I've worked with some of the popular answers to similar questions here on SO, though none to any real success.

flowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
flowLayout.itemSize = UICollectionViewFlowLayoutAutomaticSize
Of course I am familiar with Apple's repeated claims that a UICollectionViewCell in a UICollectionViewFlowLayout can be self-sizing based on internal constraints. But I have never seen working code from Apple that demonstrated this feature, and I have never once myself succeeded in confirming those claims to be true. I do not believe there is really such a thing as a self-sizing cell. For years, trying to get a cell to be self-sizing resulted in crashes. In iOS 10, the crashing mostly stopped, but now the flow layout was not laying out the cells correctly (which sounds like what you're seeing).
Naturally, I have filed bugs on all this, year after year.
Meanwhile, what I do is to give the collection view a delegate and implement collectionView(_:layout:sizeForItemAt:), and I suggest that you should do the same.
EDIT New in iOS 13 you can use a composable layout and abandon UICollectionViewFlowLayout altogether. Self-sizing cells work fine in a composable layout.

Related

What actually defines the height of a UITableViewCell?

I'm working on an iOS App right now and I want to build a view controller that uses a UITableView to create new events in a calendar (very similarly to how iOS handles event creation in the system calendar, actually). The table view has two sections, the first section holding a date picker and the second section holding two custom cells for entering an event name and notes via a text field and a text view. After playing around with them I managed to force-set them to the right size, but in the process I realized that I don't actually understand how iOS calculates individual cell heights, especially in a table view with multiple sections and multiple custom cell classes. So far, I've found a number of things that seem to play a role:
Contents of a cell, e.g. a text field and its constraints
Hugging priority and compression resistance priority of a cells content
Settings for row height and view height in the size inspector of the cell itself:
Arrangement and Autolayout settings in the size inspector of the cell
Settings for the rowHeight and estimatedRowHeight properties of a UITableViewController
The more I look into it, the more complex and confusing it all gets. Maybe one of you can shed some light on this shady bit of Swift magic?
Basically, the rule is that if the table view's rowHeight is UITableView.automaticDimension, then as long as the estimatedRowHeight isn't 0, you'll get automatic row heights, meaning that the height is determined by the cell's autolayout constraints from the inside out.
The settings can be made in respect to the table view as a whole (in code or in the storyboard) or for a single cell using the height delegate method.
Add your constraints in the cell in right way.
don't use tableview "height for cell" delegate method.
use this in your viewDidLoad
self.tableView.estimatedRowHeight = 44.0
self.tableView.rowHeight = UITableView.automaticDimension
I would say that table view has a bit tricky.
Originally it needed to know size of cell before the cell was created.
The height of cell is defined by UITableViewDelegate optional function tableView(_:heightForRowAt:)
If this function is not defined (or delegate is set to nil) then it will take value of tableView.rowHeight
For performance reasons there was also added tableView(_:estimatedHeightForRowAt:) and tableView.estimatedRowHeight
The idea was not to calculate height of every cell during fast scrolling (such calculation may be costly) and use height that is good enough.
So that are the basics before constraints layout.
Then magic came. You can return UITableView.automaticDimension as height (by delegate method or by setting tableView.rowHeight). It will force tableView to calculate height from cells' constraints (note that constraints must define that height so very likely you want to set content hugging and resistance priority of every label, and you will encounter 'errors' in storyboard/xib).
Since that operation is costly you Apple forces you to specify estimated height by yourself. Also it's important to set that value to something that makes sense, otherwise things like programatically scroll won't work correctly.

Custom Collection view cell in swift

I am new to iOS development and I am developing a buy/sell application. I am trying to make a collection view where I don't hard set the size of the collection view cell. Instead it would depend on the user who post up something they are selling. I realized that by hard coding the size of the boxes, everything looks "boxy". Any suggestions would be greatly appreciated.
Assuming you're using the default flow layout, there are two options:
You can have self-sizing cells if you:
define unambiguous constraints between your cell and its subviews;
do not define fixed width/height constraints for the image view;
set the estimatedItemSize of your UICollectionViewFlowLayout:
let layout = collectionView?.collectionViewLayout as! UICollectionViewFlowLayout
layout.estimatedItemSize = CGSize(width: 100, height: 100)
Auto layout will then resize the image view to fit the image, and the cell will then resize itself to fit that, accordingly. So, for example, with some random image sizes, the standard flow layout will resize the cells automatically for us:
Now this is a simple cell with a randomly-sized, solid color image in an image view and constraints of H:|[imageView]| and V:|[imageView]|, but the same idea works with complicated cells, too. You just need to make sure that the top/bottom and leading/trailing constraints of the cells are unambiguous.
Note, if you rely upon the implicit size of the image view, this assumes of course that the images are already sized consistently for the device. If not, you might want to create constraints for the image view height/width, include #IBOutlet references for that, and then set the constant for those constraints in your cellForRowAtIndexPath method. But, to manage memory efficiently, you generally want to downsize the images to an appropriate size anyway, so this is generally not necessary.
Alternatively, you can specify a delegate (a UICollectionViewDelegateFlowLayout) of the flow layout and then implement sizeForItemAt:.

Autolayout Width Constants in UICollectionViewCell doesn't work well

Below is a uicollectionviewcell, bar width must be according to number at right of it (vote count), event I log its width contants in console you can see it's calculated correctly, but on ui, two of them seems doesn't work well, if I scroll collectionview for several times it will be fixed, and then it will be fail again, how to set a view's contraint contant in a cell correctly?
code is too long but I want to summarize here
1) in viewcontroller's cellForItem
cell.setup()
cell.layoutIfNeeded()
return cell
2) in cell's setup()
setupBarWidths()
3) in setupBardWidhts()
resetAllWitdhs -> 0
calculate and set widths
is this correct way?
It depends upon how you are setting your constants for your subview inside uicollectionviewcell.
If your cell does not update properly constraints, possible solution is there
You must provide separate width constraint for your horizontal bar, and set it every time your reload cell.
"if I scroll collectionview for several times it will be fixed"
This problem occurs when you does not handle properly constraints changing, for example,you have "if - else " clause in your code, and you don't implement one branch.
Anyway, it's much easier to provide solution if you share your collectionView(_:cellForItemAt:)
UPDATED:
UICollectionView has many bugs internal, so one possible solution is to override UICollectionViewCell and insert the following method:
override var bounds: CGRect {
didSet {
contentView.frame = bounds
}
}
See this thread for more info.

UICollectionView self-sizing-cell on iOS 8 will crash with UIDynamic flowLayout and repeating call to _updateVisibleCellsNow

I am trying to use the so called "self sizing cell" which means:
set estimatedItemSize on flowLayout
overide preferredLayoutAttributesFittingAttributes in cell class
Such as this: UICollectionView Self Sizing Cells with Auto Layout
And I need dynamic effect like this:http://www.teehanlax.com/blog/implementing-a-bouncy-uicollectionviewlayout-with-uikit-dynamics/
It works fine without UIDynamic, but I need UIDynamic. As what I see, it will call the prepareLayout and layoutAttributesForElementsInRect until die, there will be too many _updateVisibleCellsNow waiting in line.
I have no idea how to solve that, please help me in case u see. Or, if I am using those technologies in wrong way, please let me know.
Two things worked for me:
Make sure your collection view itself has layout constraints defined for placement within its superview.
I got this crash when the estimated size was larger than the final size. If I set the estimated size to a smaller value the crash stopped.
If any size or frame position changed, it will trigger all cells' preferredLayoutAttributesFittingAttributes until all cells' frames did not change.
The behavior flow is as the following:
1. Before self-sizing cell
2. Validated self-sizing cell again after other cells recalculated.
3. Did changed self-sizing cell
If second steps can't get stable all cells' position and size, it will become infinite loop.
Please reference my post here: UICollectionView Self Sizing Cells with Auto Layout

Vertically scrolling UICollectionView with self-sizing cells only displays half the cells

I've set up a UICollectionView (with the default flow layout, and vertical scrolling) with custom cells that load from a xib. The cells contain a couple of elements (a UILabel and a UIView) with constraints on them. I've set up the constraints such that the cell height grows as the label text height increases using the new UICollectionViewFlowLayout property that's available in iOS8, estimatedItemSize:
layout.estimatedItemSize = CGSizeMake(self.collectionView.frame.width, 100)
Everything works like a charm except there's one big problem: the UICollectionView loads only half the items as returned by the numberOfItemsInSection method. So, when the method returns, say, 10, my UICollectionView only displays 5 cells (but displays and lays them out perfectly).
Some relevant findings from my debugging attempts:
I've been able to force the loading of the remaining items by calling invalidateLayout or changing the number of sections from 1 to 2. But those are just debugging hacks.
Separately, everything works like a charm when I replace estimatedItemSize with the itemSize property, i.e. hard-coding the item size. But that defeats the self-sizing functionality I'd like to implement.
I'm assuming there's something wrong with how I'm thinking about self-sized cells. Specifically, I wonder if the problem has something to do with constraints.
I'd appreciate any help here.
I believe this is due to a bug in iOS8 which still exists as of iOS8.3.
Flow layout collection views with self-sizing cells will fail to display some of their content if the estimatedItemSize is too small. This is despite the fact that according to the the API docs the only requirement on the estimatedItemSize is that it be non-zero to trigger the self-sizing behavior.
Only Apple is in a position to fix this. I think the best workaround in the mean time is to put an inflated value into estimatedItemSize`.
This repo demonstrates the issue: https://github.com/algal/SelfSizingCellsDemo. (I've filed a radar at rdar://18078598.)
The obvious non-hacky solution is to implement
flowLayout.collectionView(layout:sizeForItemAtIndexPath:)
but of course that is more code than you presumably have hoped for.
I would suggest you try two more things:
Try to experiment with more plausible estimated sizes. Also try the default value, CGSizeZero.
Check what happens in preferredlLayoutAttributesFittingAttributes() by examining the layoutAttributes parameter. Maybe you find a clue to why the other cells are "crowded out".
In a custom subclass of UICollectionViewFlowLayout
override prepareLayout like so:
- (void)prepareLayout {
[super prepareLayout];
CGSize contentSize = self.collectionView.contentSize;
// Without this line, the scrolling doesn't go all the way to the bottom, right away but takes a while
[super layoutAttributesForElementsInRect:CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height)];
}
I had this exact same problem.
I was using layout.estimatedItemSize = CGSizeMake(373.0, 500.0) together with
preferredLayoutAttributesFittingAttributes( layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes!
and I got exactly the self-sizing behaviour i wanted, except I was only getting half the cells displayed.
The curious thing is when I changed the estimatedItemSize property to: layout.estimatedItemSize = view.bounds.size ALL the cells appear!!
Hope this helps. Good luck.

Resources