How does estimatedRowHeight affect performance? - ios

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.

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.

What are the effects of setting estimatedRowHeight in UITableView?

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.)

Why is estimatedRowHeight needed for self-sizing cell?

In setting up your UITableView use to self-sizing cell, you have to specify estimatedRowHeight. Why is this even needed - given that the cell height will be calculated first by the system before the cell is displayed?
Setting the row height to UITableViewAutomaticDimension (-1), tells the auto layout engine that it needs to solve for the row height. The estimated height gives the engine a starting guess for solving the layout constraint equations.
From the documentation for the 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.
When you create a self-sizing table view cell, you need to set this property and use constraints to define the cell’s size.

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.

UITableViewCell's rectForSection returns estimate when section not visible

I'm currently trying to add infinite scrolling to a UITableView, which contains a number of calendar events. Since the event title doesn't always fit inside a single line I've added a multi-line UILabel to the cell. In order to calculate the height of the cell I'm taking advantage of UITableView's new self-sizing cells via Auto Layout in iOS 8. WWDC Session 226 talks about this in more detail.
In order to implement the infinite scrolling mechanism I'm overriding layoutSubviews where I need to calculate the height of a section which at the time given ist not visible on screen. This can be done by using [self rectForSection:0]. When doing so the table returns a height based on the estimated row size I had to define inside the table's initialiser in order to make self-sizing cells work.
self.estimatedRowHeight = 44.0;
self.rowHeight = UITableViewAutomaticDimension;
When the section gets on screen I get the correct size of the section, but since I have to update the table's contentOffset based on the computed height of that specific section this causes my table to jump up and down.
Any ideas on how to solve this? Is there a way to force a section to return the actual height and not the estimated one?

Resources