I'm trying to set the height of a UITableViewCell based on some dynamic content. I know you can "set" the height via the heightForRowAtIndexPath delegate method in your view controller, but... I can't do that!
The problem is that the height of the cell isn't known until cellForRowAtIndexPath is called, and heightForRowAtIndexPath is called before cellForRowAtIndexPath.
So, I need to somehow either reverse the order in which these two methods are called, or find another way to set the cell height.
Any ideas?
Sorry - there's no built-in way to reverse those methods or anything similar. One of the reasons is that a table view can want to know the height of a row for a number of reasons, not all of which include displaying a cell - -tableView:heightForRowAtIndexPath: might get called several times, or -tableView:cellForRowAtIndexPath: might not get called at all following a call to the height method.
What you can do is find another way to compute the appropriate height ahead of time, cache it, and rely on that cached value for -tableView:heightForRowAtIndexPath:. I've managed to do something similar by constructing a "dummy" UITableViewCell instance, keeping it out of the normal reuse queue, and just using it for layout and height-determination purposes. Such a solution would go something like this:
Get -tableView:heightForRowAtIndexPath:.
Look up the index path in your height cache. If you have a value, return it.
Otherwise, use your dummy cell to lay out the content that would appear at that index path. Measure the cell's height and cache it.
Potentially repeat steps 1-3 several times, depending on how much -tableView:heightForRowAtIndexPath: is called.
Get -tableView:cellForRowAtIndexPath:.
Dequeue a cell from the normal reuse queue, populate it, lay it out, and return it. (You may choose to verify that its height matches your precomputed height, so that the table view behaves how you'd expect.)
Depending on the content of your cell and the complexity of the phrase "lay it out" in your use case, this may incur a reasonable performance hit as you calculate the first several heights, or as your users scroll your table view rapidly. However, as your cache warms up, you should be computing fewer and fewer heights as your app continues to run.
One last point: remember that for cells with default heights (which may or may not appear in your table), you can early-out by returning tableView.rowHeight, rather than computing the default height every time it appears. This can ease the computation burden of the above approach somewhat.
Related
Nowadays fortunately it's trivial to have an iOS table where every cell has a dynamic height. So in the cell vertical constraints ..
---- top of content view
- vertical constraint to
-- UILabel, with, .lines set to zero
- vertical constraint to
---- bottom of content view
Assume the UILabel texts vary greatly one word, 20 words, 100 words,
In the table set
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 200 // say
and you're done, these days it works perfectly of course.
However, I had the common situation where you load the table, imagine ten cells.
I populate the UILabel with "Loading..."
Only then - say, a second or two later - do we get the information for the text content. It arrives say a second later and the cell changes the text to "Some long text .. with many lines".
In fact I was surprised to learn it seems UITableView does NOT handle this. The cell in question gets stuck on the original short height.
So, after the larger text is set, I tried all permutations of the usual:
maintext.sizeToFit()
contentView.layoutSubviews()
contentView.layoutIfNeeded()
on the cell, doesn't work.
I tried sending a setNeedsLayout and/or layoutIfNeeded to the table itself, doesn't work.
I thought about .reloadData() on the table itself but - doh - that would again trigger the content being drawn from the server and loaded again, so that's not ideal.
Please note that:
Obviously there are any number of workarounds for the specific example such as not using dynamic data
I am completely aware how to manually animate the height of one cell (like when you "expand" one to show something else when the user taps)
This question is about autolayout and table view - which, thanks Apple, nowadays flawlessly handles completely dynamic cell heights involving UILabels with lines zero.
But what about if the text in such a label changes?
It seems that the table view system does NOT handle this.
Surely there's a way?
When the content of a cell changes the layout (in this case, the height) you must inform the table view that the layout has changed.
This is commonly done with either:
tableView.beginUpdates()
tableView.endUpdates()
or:
tableView.performBatchUpdates(_:completion:)
Why is that not triggered automatically?
I suppose it could be to allow you to do your own animation, or you may want to delay the update, or some other reason that doesn't come to mind at the moment.
Or, it may be due to maintaining backward compatibility?
I don't know. I imagine Apple could tell us...
I have a UICollectionView for which cell heights can vary, while the width is full-width (I'm not using a UITableView because I'll need a different layout for iPad in the near future).
Straight to the point first: the weird thing is that dequeueing doesn't really ... dequeue. Cells never really get reused: if there are 40 items, 40 cells are getting initialised, and only then are they reused. I added a print("initialising cell") call in the cell's init method, and it prints as many times as there are cells.
I'm currently using auto-sizing cells, but I've also used the "keep a cell dummy property around to calculate the size" approach, and the outcome was the same. I even pre-calculated all the heights, before showing any data, basically not spending any time in preferredLayoutAttributesFitting or sizeForItemAt.
With self-sizing:
I have set the estimatedSize on the flow layout to CGSize(width: view.width, height: 1) - if I use a "real" average height, the contentOffset jumps around when scrolling;
I'm overriding preferredLayoutAttributesFitting, and here I'm calling let newAttributes = super.preferredLayoutAttributesFitting(layoutAttributes), then, before returning them, I'm setting newAttributes.width = layoutAttributes.width, since that's the width I actually need;
I have also tried not calling super, but layoutIfNeeded and systemLayoutSizeFitting, but the results were the same, even slightly worse
I also tried this, but by returning the cached sizes if they were already calculated, but the results were the same;
I also tried this, but by calling super as well, but the results were the same;
Without self-sizing:
Calculate the heights in sizeForItemAt with my cell dummy;
Pre-calculate all the heights before displaying any data, and returning those in sizeForItemAt.
Replacing the text view with a label:
if I don't use preferredMaxLayoutWidth, it has the same problem;
if I do set it (no matter if in init or layoutSubviews), the problem is there at first, but goes away when reusing starts.
The text for the UITextView is 99% of the times under 5-6 rows, and I'm using it for the link detector.
I checked Time Profiler thoroughly, and there are spikes, but not where I'd expect them. It appears that on each spike, the most time is spent in cellForItemAt.
This is an average sample of the spikes:
UIView(CALayerDelegate) layoutSublayersOfLayer has a whopping 76% Weight;
layoutSubviews has 37%;
cellForItemAt has 26%;
The init of the cell has 18.5%;
The init of my UITextView subclass has a whopping 9.2%, half of the whole cell's init;
My update cell method has 7.4%;
_updateConstraintsAsNecessaryAndApplyLayoutFromEngine has 27.7%;
There's a UIView(internal) _didMoveFromWindowToWindow at 10%;
Some image assignment at 16%, but removing any image assigning code doesn't reduce the stutter whatsoever.
All the relevant code can be found here: https://gist.github.com/rolandleth/ab5453b39a2cfe5c83119cb79ac3dc09
If any other info is missing/is required, please let me know.
Edit: The CreateCell (which has just an imageView and a textView) behaves exactly the same, so the number of subviews/constraints isn't a culprit here - I'm just eliminating any possibility I find.
I am trying to create a table to show posts by users with possibly variable lengths. I want each cell in my table to resize to fit the content of the UILabel containing the text of the post. Currently I am doing this with auto-layout, programmatically setting constraints and calculating the height of the table view cell based on the height of the UILabel. Everything looks as I want it to, except when rapidly scrolling through the table the performance is horrendous, with the CPU getting fully maxed out and unable to keep up with setting the constraints for each cell as they are reused and placed with new text.
I was wondering if anyone knows of a better way to do this. Is there a way I can continue to use auto layout to size my cells without sacrificing performance? Or would the best solution be to create different sized cells with different identifiers, and just choose the best fit based on the UILabel text size.
I also read somewhere that UICollectionView has much better performance in displaying variable-sized content, would it be worthwhile/possible to try configuring a UICollectionView to display the messages, and somehow make it look like a UITableView?
Essentially I just need a suggestion on how to display messages of variable sizes (up to about 7 or 8 lines of text max) in a TableView-like manner without causing massive slowdowns as the user rapidly scrolls through the table.
Thank you
The best trick in the book for things like this is cacheing. Add to your model anything that's expensive to compute (not the views themselves, but the computational results, like view bounds sizes). Make your datasource methods all about looking up and assigning, not at all about computing.
The way I had implemented adding the constraints, the constraints were updated whenever [self updateViewConstraints] was called, regardless of whether or not the cell had already had its constraints calculated and applied. I resolved this by adding a BOOL property to my custom TableViewCell called didUpdateConstraints, and setting it to YES as soon as constraints are updated the first time. Then in my [self updateViewConstraints] method I only update the constraints if !self.didUpdateConstraints
Now that constraints are not being needlessly updated when all views have already been correctly constrained, the performance is significantly better, and I don't observe any slowdown upon scrolling through the table.
It seems that UITableView does not auto-calculate the exact height needed to cover all the visible cells, instead, it substitutes any extra area with empty rows.
I'd like to calculate the exact height needed to cover only the visible cells, as to rid my UITableView of those ugly empty rows. I am quite aware that this can be done manually using the Interface Builder, however there must be a more efficient and dynamic approach to this problem.
You can use the table view's content size to determine how much space the table wants.
CGFloat height = myTableView.contentSize.height;
This height will automatically consider headers, footers, and the size and number of cells, and pretty much anything else. And it's all dynamic, so if you decide to go to a 7 row table in the future, it will still work without a problem and without needing to change that piece of code. The only time you would need to change it is if you decided at some point that you no longer want to show the whole table anymore.
And a quick tip - you'll probably want to also set [myTableView setScrollEnabled:NO]. Disabling scrolling will prevent the table from "bouncing" if the user does try to scroll it - I just think that bouncing looks really silly if all the content of the table is being shown at once.
You can then use this height to either
adjust the constraints on your table view (if you are using Autolayout)
adjust the frame of your table view (if you use the old springs-and-struts approach)
Use the following code to get set cellHeight member variable and use it where ever required.
_cellHeight = floorf((CGRectGetHeight(self.tableView.bounds))/5);
This should give you height of each row and it will handle all cases.
The above code has to be used in following methods.
-(void)viewWillLayoutSubviews;
- (void)viewWillAppear:(BOOL)animated;
I hope this helps.
I'm struggling a little bit with the size for cells in UICollectionView.
In android, you can easily "wrap" the size of the cell.
Just like in iOS, you have a function call 'GetCell' and you decide how big it will be.
The difference in iOS is that in the "getCell" function (of UICollectionViewController) it seems you can't choose the size of the cell (or the contentview). If I change the size, it will ignore it and use anyway the general 'ItemSize' of the CollectionView (which is the same for all cells).
This sometimes results in Views which are not very beautiful. For example, if I have a horizontal list with images, I want the distance between images to be the same, independent if one image is 200x200 and the other 400x200. So the cell size should be different also.
It is possible to define a different size for different cells. You can use the Collectionview delegate and the GetSizeForItem (= sizeForItemAtIndexPath in ObjC) function. The problem is, this function is called BEFORE the actual GetCell function.
So if I have a more complex Cell, with for example some labels. In my "GetCell" function, I build this Cell and at the end, when returning the Cell, I know which size it should be. However, in the GetSizeForItem function, that info is not available yet, because the Cell is still 'null'.
The only way I could do it, is to actually build the UIView for the cell (so I can request the size) at the moment of the 'GetSizeForItem' call. But this doesn't seem a good design, because I'm building the UIView before the 'GetCell' where I will build it again.
Is there something I'm overlooking?
Regards,
Matt
Indeed GetSizeForItem gets called separately from GetCell. It's done that way because creating UIViews is a very time and memory consuming task, and your application would either run out of memory or have to dispose other views to be able to handle big lists.
Before the view gets presented, the UICollectionView (and UITableView) asks for the sizes and positions of all (or most) elements in the list, so it can know where to draw them. Many of those elements won't be visible though, so the collectionView avoids having to create them. This is why the GetSizeForItem gets called upfront, and the GetCell only later.
In your case, try to separate the logic that calculates the size of the view from the view itself. Make it a simple math formula that doesn't require a view to exist, so it's fast enough to be run upfront.