Where to determine the height of a dynamically sized UICollectionViewCell? - ios

I'm using UICollectionViewFlowLayout. My cells contain UILabels that differ in height (number of lines).
It seems that the best way to get the cell height would be in the subclass of UICollectionViewCell, because that is where I set the layout and have access to intrinsic size of my views, BUT:
collectionView: layout: sizeForItemAtIndexPath: is called before the collectionView: cellForItemAtIndexPath: delegate method, which means that I need to know the cell height before I have the actual layout of the cell.
Everything I came up with so far seems too complicated, like starting with fixed cell height, referencing actual height after labels in the cell load and reloading the data again with correct height. Is there a better way to do this?

Unfortunately, no.
For dynamically sized items in UICollectionView you need to know or compute the size of the cell before it is created. The traditional way to do this is to store the data for each row in an array and then compute the size of that data in collectionView:layout:sizeForItemAtIndexPath:.
For example, if you had an array of text to be displayed you could store the NSString objects in an array, measure that string in collectionView:layout:sizeForItemAtIndexPath: and return the size. UICollectionView then takes that size and calls initWithFrame: or setFrame: when configuring your cell's view.
It's also not a bad idea to cache these sizes if they don't change often.

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.

Intrinsic content sized UICollectionView inside UITableView

Let me preface by saying that I've spent probably 100 hours over the past few months searching Google and StackOverflow trying to answer this question. I've followed many tutorials and have not been able to solve my problem. Every one is a major disappointment and is disheartening.
Expectation
Reality
Problem
I have a UITableView that contains a UIView. The UIView has auto layout constraints to pin it 15pt off the edges of the cell, and then I give that a shadow and rounded corners. Inside that UIView, I have a few labels, then a UICollectionView, and a stack view.
My UICollectionView keeps having a height of 0 and I can't figure out how to make it have intrinsic size. The only way I can seem to make it display properly is to force a specific height to it via auto layout inside the storyboard (which is what I did to take this screenshot). Unfortunately, forcing a height like this is not realistic for 2 reasons:
The height varies based on the width of the device. Since the images are squares, the height is loosely (but not exactly) half the width of the device.
If there are no images, the height of the collection view should be 0.
It really seems like it should be possible to use the intrinsic content size of the flow layout here.
Storyboard
Constraints
I've got about 15pt of space between each of the labels, collection view, and stack view (so, the vertical space). The collection view prototype cell has an image view in it. The image view has top, bottom, trailing and leading space all set to 0 to the collection view cell, and also has a 1:1 ratio.
Collection View Layout
To achieve the layout of one large image and 4 small images, I'm using SNCollectionViewLayout.
Data Source / Delegate
In my controller, in viewDidLoad, I've set the estimatedRowHeight of the UITableView to 400, and I've set the rowHeight to UITableView.automaticDimension.
In the tableView cellForRowAt, I dequeue a reusable custom UITableViewCell class, set various outlets on the table view cell, and call reloadData() on the collection view for the table view cell. In the storyboard, I've set the table view cell as the collection view's data source and delegate.
In the table view cell's awakeFromNib function, I've got it setting the corner radius, shadow, and using the following code for the flow layout on the collection view (which you can see in their example):
let snCollectionViewLayout = SNCollectionViewLayout()
snCollectionViewLayout.fixedDivisionCount = 4
snCollectionViewLayout.delegate = self
snCollectionViewLayout.itemSpacing = 10
collectionView.collectionViewLayout = snCollectionViewLayout
I've also implemented the optional SNCollectionViewLayoutDelegate protocol by writing the following function:
func scaleForItem(inCollectionView collectionView: UICollectionView, withLayout layout: UICollectionViewLayout, atIndexPath indexPath: IndexPath) -> UInt {
if indexPath.row == 0 || collectionView.numberOfItems(inSection: indexPath.section) <= 2 {
return 2
} else {
return 1
}
}
This function is why the first image is twice the height and width as the rest.
Research
In trying to figure this out, I've checked the collectionView.collectionViewLayout.collectionViewContentSize inside the table view cell's cellForRowAt and willDisplay methods. In either case, it's (0,0).
I've also tried checking it in the collection view cell's cellForItemAt and willDisplay. Those log lines don't even print unless I add a height constraint to the collection view to force it tall, but then I don't have a good way to bring the height back down to what it should be, and any time I've tried, I got conflicting constraint warnings and really wonky stretched views (such as the stack view being totally squished).
In testing, I've forced a height constraint that's large enough for the collection view to fit, and when that happens, it renders correctly and the collection view content size is correct, so I know it's capable of keeping track (which can be seen here).
Does anyone have any insight into this? I'd be happy to provide more code if you have any specifics on things to look into. I tried to give the full picture, while keeping this as short as possible (I know it's long).
I feel like the issue has something to do with the collection view being inside a table view cell.
Thanks in advance.
Keep a height constraint connection in your table view cell.
#IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
And update the height constraint value programatically as per height calculations
self. collectionViewHeightConstraint.constant = 100 // set it to 0 if nothing to display

Calculate UICollectionView contentSize in tableView's heightForRowAtIndexPath

I have a collectionView inside a UITableViewCell. The collectionView's content is dynamic, hence, its height is dynamic too and the tableView cell's height depends on the collectionView's height. The collectionView cells have different sizes. The collectionView's height is always set to fit its contentSize height.
My problem is that the tableView's function heigthForRowAtIndexPath, is called before the collectionView is created (in cellForRowAtIndexPath) so, to return the height that fits the collectionView, I need to calculate it manually (which doesn't seem like a good idea for a collectionView with cells of different sizes).
I tried to use autolayout in order to use UITableViewAutomaticDimension but it didn't work (maybe I did it in a wrong way).
What is the best aproach to make a UITableViewCell consider the height of its subview in heightForRowAtIndexPath? Can I know a collectionView's estimated size without creating it?
Use self sizing, which is available in iOS 8. There are plenty of good tutorials online, like this one: http://www.appcoda.com/self-sizing-cells/.
The idea is that you can use auto layout and a few lines of code in viewDidLoad to render a table view cell that dynamically fits the content in it.
Some more tutorials:
http://useyourloaf.com/blog/2014/08/07/self-sizing-table-view-cells.html
https://github.com/smileyborg/TableViewCellWithAutoLayoutiOS8

Calculating sizeForItemAtIndexPath in UICollectionView without using the cells

I am in a loop with myself on from where to get the cell's size in a collection view in case I have a cell with auto layout
I understand that sizeForItemAtIndexPath should get the size from the data and not from the cell itself, because the cell is not drawn yet, but in auto layout I can't calculate this just by looking at the data, unless I put in code the contraints in a way that I can later calculate from them (seems crooked)
On the other hand, UIView does have intrinsicContentSize and systemLayoutSizeFittingSize which gives the size of a view that's already drawn or going to be drawn.
But in sizeForItemAtIndexPath I don't have a view yet, and the data is just data.
the cell nib looks like this
In what way should I get the size of a custom cell (from .nib)?
I could technically hard code sizes for the images and icons, but that feels wrong. It also feels wrong to ask the view what size it is in a function that should tell the controller what size it is
You can use an off screen prototype cell which you configure and then use to determine its size.
This is a common approach for UITableView pre iOS8 to determine cell height. The same approach should work for UICollectionView.
See the accepted answer here: Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

Have UITableViewCell resize itself with autolayout

I have 3 labels in a UITableViewCell and have the labels set so they will wordwrap. If they word wrap the text goes into the next cell. How do I get AutoLayout to expand the cell based on the content without having to write code in the heightForRowAtIndex method? Isn't there a constraint I can use to automatically adjust the cell based on the contentView?
The cell looks fine if the text doesn't wrap in the label. Once it wraps that is when the problem occurs and I would like to have the cell resize to fit the content and have the same spacing between the bottom label and the bottom as there is between the top and top label.
Unfortunately no, you can't do this. A table view calculates its own total height first and has a fixed idea of the size of each cell as they load, it won't determine it's height from the outside in and it won't let layout constraints change the height of a cell.
If you think about how tables work, with cell reuse, then you couldn't really size the table from its cells without loading in every cell and adding it to the scrollview, and performing a layout pass on the whole thing. That would probably lead to quite poor performance.
You could experiment with populating a "free" cell (i.e a cell you've just instantiated, not added to a table) and laying it out for each row in your datasource when calculating heightForRow.
As you are loading the individual cells, after you fill the labels, but before you load the instance of the cell, check the label height.
Something like:
cell.frame.size.height
If the height is large enough that you know the label has wrapped to two lines, then increase the height of the cell you are about to load.

Resources