What are the effects of setting estimatedRowHeight in UITableView? - ios

It seems that whatever I set in estimatedRowHeight, there isn't any visual differences. Performance wise, I'm not sure how setting a value such as 1 vs 100 makes any difference. The document just says setting a non-negative will give you a better performance, but doesn't elaborate more.

In order to perform its full initial internal layout and thus paint the scroll indicators correctly, the table view must know the heights of all the rows (as well as any section headers etc.).
By supplying the estimatedRowHeight, you allow the table to perform a full initial internal layout knowing only the heights of the visible rows (because it uses the estimated height for all the other rows). For a table view with variable row heights, that's much, much faster.
(Of course there is no point to the estimatedRowHeight if the table view's rowHeight is in fact the height of every row.)

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.

Multiple flexible height items - how to set the autolayout priorities?

Say you have (for example) a table cell layout with more than one dynamic flexible-height items,
They are linked vertically one to the other in the obvious way.
It seems very difficult to make this work, and very undocumented.
You'd expect that: you set the compression resistance of all the expandable items to 751. But that doesn't work.
After random experimentation, it seems to me that surprisingly you have to do something like this:
have the compression/hugging of the overall view on 250 and 750,
then strangely enough, for the three text views in the example, the priorities have to
...sequentially increase...
And, I think you have to make one of them "one lower" than the overall view - in the example one of them would be 749.
It's difficult/impossible to find the exact "formula" to make it work consistently.
What the heck is the logic of this? Is it just a pure bug in iOS?
Has anyone found the "correct formula" for making a number of expandables work in a cell?
priorities for the three text views
priorities for the overall holder view
cheers
Your question is not very clear to me. However, here's a good recipe to stack text views (whether or not you use a stack view for constraining them doesn't matter):
1. Disable scrolling for all your UITextViews:
Reason: If scrolling is enabled, a UITextView does not have a defined intrinsicContentSize. See this answer. Without an instrinsic content size, the content hugging and compression resistance ("CHCR") priorities have no meaning or effect.
2. Give each view along one axis a different priority for CHCR:
More precisely: Give all views that are connected with constraints along one axis a different CHCR priority that are not constrained in size along that axis (i.e. that don't have a fixed width / height constraint).
In your particular example, your setup shown in the screenshots is a correct solution:
Each of the three text views has a different content hugging and a different compression resistance priority. As a general rule you should start with the default values (750 / 250) and only slightly increase or decrease them as you did.
In case you use a table view with self-sizing cells (by setting its estimatedItemSize), the CHCR priorities won't matter at runtime because the cell will automatically resize to accommodate enough space for all three text views. You just need to set those priorities to "silence" Interface Builder.
If you use fixed-height table view cells however, the CHCR priorities are quite important because they determine
which of the views will shrink first if there's not enough space inside the cell (it's the view with the lowest compression resistance priority) and
which of the views will expand if there's more space inside the cell than the subviews actually need (it's the view with the lowest content hugging priority).
The CHCR priorities of the superview ("MV") is irrelevant as it's usually just a container view that does not have a defined intrinsicContentSize either. Its size is defined by the inner and outer constraints you add.
For more information on the CHCR priorities, see the chapter Intrinsic Content Size in the Auto Layout Guide.
When I am playing with cells that have a variable height I use:
tableView.estimatedRowHeight = 20.0
tableView.rowHieght = UITableViewAutomaticDimension
These two cause the table view render its cells with the appropriate height. In order for the height to be computed though you should set the cell's data in tableView(cellForRowAt)

How does estimatedRowHeight affect performance?

When use dynamic UITableViewCells height calculation via UITableViewAutomaticDimension you must set estimatedRowHeight. Is there any information about how does bad estimated value affect performance?
Estimated row height is not as important as you necessarily think. It is mainly used by the table to configure certain UI elements like it's scroll bars for instance. When tableView.reloadData() is called the table recalculates this anyway. As a general rule I just use an estimated row height of either 44 or whatever the height of my placeholder cell that is displayed whilst I am loading data is. The performance repercussions of using an inaccurate estimatedRowHeight are minimal to non-existent as it does not actually contribute to calculating the dynamic height of cells marked as UITableViewAutomaticDimention.

iOS Calculate Height For Cell

I have a problem with Apple's dynamic resizing cells such that the logic for the layout of my cells isn't as cut and dry as just a few stacked growing UILabels.
As a result, I can't really use the dynamic resizing option they've provided and so I need to manually calculate the height of my cell using NSString boundingRect methods.
That's fine - it works, but I end up needing to store a lot of constants that keep track of my auto layout constraints. I feel like this is counter intuitive to what auto layout is supposed to do for me, so I'm not sure if this is the correct way to implement heightForRowAtIndexPath.
I essentially have to go and copy my constraints into a constant and then use those values in a class method to generate my heights. Apple provides very little internal insight into how UITableViewAutomaticDimension works, but it's clear that the height of the cell is still calculated BEFORE it is laid out. Thus I can't really add any complex logic to it unless I know what methods are called before.
Any ideas on what I should do, or if my approach is ok?
The common solution I can offer is to add a height constraint fro your cell and change the constraint according to your needs, whatever they are. UITableViewAutomaticDimension will resize cell to the height you specify with this constraint automatically.
If your table cell is custom then its easier if you use a method like configureCell and pass the indexPath to it from cellForRowAtIndexPath and then determine the layout based on the data that you have and see where you want the left and right label to be placed. Once you have done this store the height required in the model or perhaps another array that has the same number of rows as your table and use it to return in heightForRowAtIndexPath.
This is easier and gives you flexibility without having to fiddle with too many delegate methods of table view. Centralise your layout logic in one place.
Another alternative is to override layoutSubviews of the table view cell and calculate the height there and store it.
If you want your tableview cell to be assigned a default height, you can use estimatedHeightForRowAtIndexPath: available in UITableViewDelegate. As per Apple guidelines:
// Use the estimatedHeight methods to quickly calcuate guessed values which will allow for fast load times of the table.
// If these methods are implemented, the above -tableView:heightForXXX calls will be deferred until views are ready to be displayed, so more expensive logic can be placed there.

Scroll UITableView to bottom when using estimated row heights

I'm having a problem that seems like it shouldn't really be a problem. I'm trying to create a comments ("chat") view for my app, but I'm using estimated row heights and can't find a nice way to have the comments start at the bottom, such that when the view is loaded, the latest comment is at the bottom of the screen, just above the input area also at the bottom of the screen.
I've been looking around for ages and have found solutions like these:
[self.tblComments scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Doesn't work nicely because it uses the estimated row height, which isn't always right (some comments will be much longer).
[self.tblComments setContentInset:UIEdgeInsetsMake(self.tblComments.bounds.size.height - self.tblComments.contentSize.height, 0, 0, 0)];
Same problem as above (contentSize is determined by estimatedRowHeight).
I'm sure there's a perfect solution to this somewhere since so many apps have views like this, I just can't find it. I'd love any insight into something I may be missing here...
Edit:
contentSize is simply "The size of the content view." But since you've set a value for estimatedRowHeight, all your off-screen cells are assumed to be of height estimatedRowHeight.
According to the docs, here's the point of estimatedRowHeight:
Providing a nonnegative estimate of the height of rows can improve the performance of loading the table view. If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
So basically, in order to save time, the table is making an assumption about row height and thus content size based on your estimatedRowHeight. By using estimatedRowHeight you're asking your UITableView to make these "shortcuts" in order to improve performance. You're basically telling the program not to calculate the row heights ahead of time, so in order to get the row heights and content sizes of any off-screen content, you have to calculate them manually. As long as you decide to set an estimatedRowHeight, this is unavoidable.
My guess is that many chat apps you've looked at don't use estimatedRowHeight in order to avoid this issue...
But if you do in fact feel as if using estimatedRowHeight helps with your app's performance, here are two suggestions.
SUGGESTION #1
Perhaps it makes sense to have a totalChatHeight variable where you keep track of the total row heights as they come in, calculated using similar logic as your heightForRowAtIndexPath: method. By originally calculating this info in the background as the comments load then incrementing as you go, you'd still maintain the performance benefits of using estimatedRowHeight without sacrificing the functionality of having the table know its row heights.
By doing this, you can structure your contentOffset statement like so to scroll to the bottom using the known table row height information:
CGPoint bottomOffset = CGPointMake(0, totalChatHeight - self.tblComments.frame.size.height);
[self.tblComments setContentOffset:bottomOffset animated:YES];
SUGGESTION #2
Another suggestion would be to calculate your row average dynamically so you have a more precise measurement for estimatedRowHeight and the contentSize of your UITableView would approximately be the same with or without using estimatedRowHeight. That way estimatedRowHeight is actually the estimate row height and the estimated content size should also be near the actual contentSize, so this standard contentOffset formula should work:
CGPoint bottomOffset = CGPointMake(0, self.tblComments.contentSize.height - self.tblComments.frame.size.height);
[self.tblComments setContentOffset:bottomOffset animated:YES];
This could potentially be dangerous though as both the chat log and rounding error grow.
SUGGESTION #3 <-- Perhaps the best way to go in this case
This third suggestion may be the easiest to implement in your case as it doesn't require any ongoing calculations:
Just set estimatedRowHeight to the last row's height before reloading the table's data.
You can calculate that new row's height using whatever algorithm you use in heightForRowAtIndexPath: then set estimatedRowHeight to that height; that way you can use this statement to scroll to the last cell
[self.tblComments scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
and even though, as you said, it determines the row position based on estimatedRowHeight, it should scroll to the correct position as long as the estimatedRowHeight equals the bottom row height.

Resources