UITableView scrolls to top when reloading cells with changing cell heights - ios

I have a table view which contains a placeholders while it loads in images. When the image is loaded, I call reloadRowsAtIndexPaths:withRowAnimation:. At this point, the cell changes height, based on the size of the image. When that happens, I want the table view's content offset to remain in place, and for the cells below to be pushed further down, as you might imagine.
The effect I'm getting instead is that the scroll view scrolls back to the top. I'm not sure why this is, and I can't seem to prevent it. Putting beginUpdates() before and endUpdates()after the reloadRows line has no effect.
I am using estimatedRowHeight, as is needed as my table view can potentially have hundreds of rows of different heights. I am also implementing tableView:heightForRowAtIndexPath:.
EDIT: I've set up a demo project to test this, and admittedly I can't get the demo project to reproduce this effect. I'll keep working at it.

It's an issue with the estimatedRowHeight.
The more the estimatedRowHeight differs from the actual height, the more the table may jump when it is reloaded, especially the further down it has been scrolled. This is because the table's estimated size radically differs from its actual size, forcing the table to adjust its content size and offset.
The easiest workaround is to use a really accurate estimate. If the height per row varies greatly, determine the median height for a row, and use that as the estimate.

Always update the UI on the main thread. So just place
[self.tableView reloadData];
inside a main thread:
dispatch_async(dispatch_get_main_queue(), ^{
//UI Updating code here.
[self.tableView reloadData];
});

I had the same problem and decide it by this way: save heights of cells when they loads and give exact value in tableView:estimatedHeightForRowAtIndexPath:
// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary;
// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
[cellHeightsDictionary setObject:#(cell.frame.size.height) forKey:indexPath];
}
// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
if (height) return height.doubleValue;
return UITableViewAutomaticDimension;
}

I was seeing this, and the fix that worked for me was to choose an estimated row height that is the smallest of the possible rows. It had originally been set to the largest possible row height when the unintended scrolling was happening. I am just using the single tableView.estimatedRowHeight property, not the delegate method.

Related

UITableView Rows Height After Loading Cells

I have UITableView that contains 4 different types of customized cells in storyboard. Each cell has customized UILabels which get variable amount of text data from backend. I am struggling with making the cells resizing correctly. I would really want to change the height of each cell but I can not use heightForRowAtIndexPath because it is called before cellForRowAtIndexPath, but the height is actually calculated within each customized cell.
I tried writing in each cells' height into an array while the UITableView is loading, then just reloading it all over again once, but no effect. I tried using CGFloat rowHeight = UITableViewAutomaticDimension with no success either. The customized labels in each cell definitely grow with text which I see when I just statically change row height to higher numbers. So, I would need somehow my labels to push on rows to make them grow, not sure.
Different similar posts on stackoverflow that I found did not help.
The issue was that I needed to set up top and bottom constraints to the ContentView and NOT to the cell itself in the storyboard.
Label -> ContentView top and bottom constraints need to be set up. And then UITableViewAutomaticDimension specified in viewDidLoad:
self.tableView.rowHeight = UITableViewAutomaticDimension;
estimatedRowHeight should be set too. For example:
self.tableView.estimatedRowHeight = 76.0f;
First Method called is:
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
Second:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Then:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
set a break point in the above methods and test it. So if you want to preset the height use estimatedHeightForRowAtIndexPathmethod.

How do I tell my UITableViewCell to "auto-resize" when its content changes?

In my cell.xib, I have a label, with constraints to all its sides. I've set that label to lines = 0 and line-break = word wrap. Then, I do this to my TableView:
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 100.0
Everything works great, and my UITableViewCell is auto-height. If the text is long, then my tableView intelligently calculates the size.
The problem is -- how do I tell my UITableView to "re-calculate" the size once the content changes in the cell?
My cell could call its delegate, and in this delegate, I'd like the TableView to re-draw the height.
Right now, the content in my cells change constantly, but the cell height never changes.
There is a documented way to do this. See UITableView.beginUpdates() documentation:
You can also use this method followed by the endUpdates method to animate the change in the row heights without reloading the cell.
So, the correct solution is:
tableView.beginUpdates()
tableView.endUpdates()
Also note that there is a feature that is not documented - you can add a completion handler for the update animation here, too:
tableView.beginUpdates()
CATransaction.setCompletionBlock {
// this will be called when the update animation ends
}
tableView.endUpdates()
However, tread lightly, it's not documented (but it works because UITableView uses a CATransaction for the animation).
I've found the best way to get it to check heights is to call, after whatever text change has been made, in order:
self.tableView.beginUpdates()
self.tableView.endUpdates()
This causes the tableView to check heights for all visible cells and it will cause changes to be made as needed.
I think the simplest solution is to reload that specific cell. For example:
- (void)yourDelegateMethodOfCell:(UITableViewCell *)cell {
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
//If cell is not visible then indexPath will be nil so,
if (indexPath) {
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
You can get automatic cell height by this code
tableView.beginUpdates()
// add label text update code here
// label.numberOfLines = label.numberOfLines == 0 ? 1 : 0
tableView.endUpdates()
Below is the reference to this solution with demo :
GitHub-RayFix-MultiLineDemo
I got your point, and I met the problem before.
Your cell is in AutoLayout, and you wish the cell changes by itself. Here is recommended answer for that: Using Auto Layout in UITableView for dynamic cell layouts & variable row heights , so we don't talk about that again.
So here we focus on your problem.
Since the content of your cell changes constantly, which means the content has updated. Here we suppose the content is a label. We set the label's text, and surely the label's height maybe change.
Here comes the point: How does the label's change inform the cell to update?
We use AutoLayout, surely we have to update the constraint of height for the label.
And I think it will work!
Below is the detail step:
1. We setup the constraints for the cell's subviews.(I think it's done)
2. One of the label's height is changed by itself.(I think it's done too)
3. We get the new height of the label, and update the constraint of height for the label.(what we have to do)
Seems you wanted to reload the particular cell/cells based on content changes
Here we have a couple of options
1) Need to reload the entire table view .
or else
2) Reload particular cell/cells based on content changes.
But the preferred option would be reloading the particular cell,
Why because
when you asked your UITableView instance to reload a couple of cells,tableview will asks its datasource(-tableView:cellForRowAtIndexPath:) to get the updated content,so that the reloaded cells will have the updated size & Updated content aswell.
Try to reload the cells when the content/height need to update based on content
Hope that helps!
Happy coding :)
Take a look at this answer : Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
How to achieve dynamic cell size is described very thorough there.
As a suggestion for testing try adding setNeedsLayout to:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
or
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
To disable the annoying tableView animation:
UIView.setAnimationsEnabled(false)
tableView.beginUpdates()
// cell.titleLabel?.text = "title"
// cell.detailTextLabel?.text = "Very long text ..."
// cell.detailTextLabel?.numberOfLines = 0
tableView.endUpdates()
UIView.setAnimationsEnabled(true)
You can resize your cell height by implementing below method only
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
return UITableViewAutomaticDimension;
}

iOS8 automatic tableviewheight wrong on first cells

I'm using Autolayout on iOS 8 and make use of UITableViewRowAnimationAutomatic and all of its magic.
When the Tableview appears the cells have a wrong height and the subviews are distributed over the whole cell with a lot of horizontal space between them. When I scroll down or rotate the device and back alls cells are drawn correctly with the correct size.
Debugger says no error and Autolayout warnings are not present.
These cells are only drawn on iPad and I have specified only for sizeclass (Regular | Regular).
Do you any hints what could be the problem?
There are three things that you need to make sure you are doing...
The AutoLayout Constraints should cover the entire height of the cell. So just by looking at the constraints you should be able to say exactly how tall the cell is.
Implement the estimated height for row method...
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// return an actual number here. This is a guess of how tall the cells are
return 100;
}
or
// Thanks #rdelmar :-)
self.tableView.estimatedRowHeight = 100;
Implements height for row...
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// return auto dimension here
return UITableViewAutomaticDimension;
}
Once you have done all three of these it will work.
See my blog here for more data (note, there has been an update since I wrote the blog which I haven't updated yet).
http://www.oliverfoggin.com/using-a-static-uitableview-as-a-layout-device/

Is there a way to update the height of a single UITableViewCell, without recalculating the height for every cell?

I have a UITableView with a few different sections. One section contains cells that will resize as a user types text into a UITextView. Another section contains cells that render HTML content, for which calculating the height is relatively expensive.
Right now when the user types into the UITextView, in order to get the table view to update the height of the cell, I call
[self.tableView beginUpdates];
[self.tableView endUpdates];
However, this causes the table to recalculate the height of every cell in the table, when I really only need to update the single cell that was typed into. Not only that, but instead of recalculating the estimated height using tableView:estimatedHeightForRowAtIndexPath:, it calls tableView:heightForRowAtIndexPath: for every cell, even those not being displayed.
Is there any way to ask the table view to update just the height of a single cell, without doing all of this unnecessary work?
Update
I'm still looking for a solution to this. As suggested, I've tried using reloadRowsAtIndexPaths:, but it doesn't look like this will work. Calling reloadRowsAtIndexPaths: with even a single row will still cause heightForRowAtIndexPath: to be called for every row, even though cellForRowAtIndexPath: will only be called for the row you requested. In fact, it looks like any time a row is inserted, deleted, or reloaded, heightForRowAtIndexPath: is called for every row in the table cell.
I've also tried putting code in willDisplayCell:forRowAtIndexPath: to calculate the height just before a cell is going to appear. In order for this to work, I would need to force the table view to re-request the height for the row after I do the calculation. Unfortunately, calling [self.tableView beginUpdates]; [self.tableView endUpdates]; from willDisplayCell:forRowAtIndexPath: causes an index out of bounds exception deep in UITableView's internal code. I guess they don't expect us to do this.
I can't help but feel like it's a bug in the SDK that in response to [self.tableView endUpdates] it doesn't call estimatedHeightForRowAtIndexPath: for cells that aren't visible, but I'm still trying to find some kind of workaround. Any help is appreciated.
As noted, reloadRowsAtIndexPaths:withRowAnimation: will only cause the table view to ask its UITableViewDataSource for a new cell view but won't ask the UITableViewDelegate for an updated cell height.
Unfortunately the height will only be refreshed by calling:
[tableView beginUpdates];
[tableView endUpdates];
Even without any change between the two calls.
If your algorithm to calculate heights is too time consuming maybe you should cache those values.
Something like:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat height = [self cachedHeightForIndexPath:indexPath];
// Not cached ?
if (height < 0)
{
height = [self heightForIndexPath:indexPath];
[self setCachedHeight:height
forIndexPath:indexPath];
}
return height;
}
And making sure to reset those heights to -1 when the contents change or at init time.
Edit:
Also if you want to delay height calculation as much as possible (until they are scrolled to) you should try implementing this (iOS 7+ only):
#property (nonatomic) CGFloat 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.
The default value is 0, which means there is no estimate.
This bug has been fixed in iOS 7.1.
In iOS 7.0, there doesn't seem to be any way around this problem. Calling [self.tableView endUpdates] causes heightForRowAtIndexPath: to be called for every cell in the table.
However, in iOS 7.1, calling [self.tableView endUpdates] causes heightForRowAtIndexPath: to be called for visible cells, and estimatedHeightForRowAtIndexPath: to be called for non-visible cells.
Variable row heights have a very negative impact on your table view performance. You are talking about web content that is displayed in some of the cells. If we are not talking about thousands of rows, thinking about implementing your solution with a UIWebView instead of a UITableView might be worth considering. We had a similar situation and went with a UIWebView with custom generated HTML markup and it worked beautifully. As you probably know, you have a nasty asynchronous problem when you have a dynamic cell with web content:
After setting the content of the cell you have to
wait until the web view in the cell is done rendering the web content,
then you have to go into the UIWebView and - using JavaScript - ask the HTML document how high it is
and THEN update the height of the UITableViewCell.
No fun at all and lots of jumping and jittering for the user.
If you do have to go with a UITableView, definitely cache the calculated row heights. That way it will be cheap to return them in heightForRowAtIndexPath:. Instead of telling the UITableView what to do, just make your data source fast.
Is there a way?
The answer is no.
You can only use heightForRowAtIndexPath for this.
So all you can do is make this as inexpensive as possible by for example keeping an NSmutableArray of your cell heights in your data model.
I had a similar issue(jumping scroll of the tableview on any change) because I had
(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 500; }
commenting the entire function helped.
Use the following UITableView method:
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
You have to specify an NSArray of NSIndexPath which you want to reload. If you want to reload only one cell, then you can supply an NSArray that holds only one NSIndexPath.
NSIndexPath* rowTobeReloaded = [NSIndexPath indexPathForRow:1 inSection:0];
NSArray* rowsTobeReloaded = [NSArray arrayWithObjects:rowTobeReloaded, nil];
[UITableView reloadRowsAtIndexPaths:rowsTobeReloaded withRowAnimation:UITableViewRowAnimationNone];
The method heightForRowAtIndexPath: will always be called but here's a workaround that I would suggest.
Whenever the user is typing in the UITextView, save in a local variable the indexPath of the cell. Then, when heightForRowAtIndexPath: is called, verify the value of the saved indexPath. If the saved indexPath isn't nil, retrieve the cell that should be resized and do so. As for the other cells, use your cached values. If the saved indexPath is nil, execute your regular lines of code which in your case are demanding.
Here's how I would recommend doing it:
Use the property tag of UITextView to keep track of which row needs to be resized.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
[textView setDelegate:self];
[textView setTag:indexPath.row];
...
}
Then, in your UITextView delegate's method textViewDidChange:, retrieve the indexPath and store it. savedIndexPath is a local variable.
- (void)textViewDidChange:(UITextView *)textView
{
savedIndexPath = [NSIndexPath indexPathForRow:textView.tag inSection:0];
}
Finally, check the value of savedIndexPath and execute what it's needed.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (savedIndexPath != nil) {
if (savedIndexPath == indexPath.row) {
savedIndexPath = nil;
// return the new height
}
else {
// return cached value
}
}
else {
// your normal calculating methods...
}
}
I hope this helps! Good luck.
I ended up figuring out a way to work around the problem. I was able to pre-calculate the height of the HTML content I need to render, and include the height along with the content in the database. That way, although I'm still forced to provide the height for all cells when I update the height of any cell, I don't have to do any expensive HTML rendering so it's pretty snappy.
Unfortunately, this solution only works if you've got all your HTML content up-front.

iOS unable to see last few rows of custom table cell when scrolling to the bottom.

Currently I have a UITableview, I have enough data that cannot be shown entirely on the screen, so user will have to scroll the table view.
The issue I'm seeing is that when I scroll all the way to the bottom, the last element shown in the table view is not really the last element
however, if I do a touch drag, and try to drag it down really hard, I can see the last element, but if I release the finger, the scroll bounced back to the element that is displayed at the bottom, but not the last element
How can I ensure tableview scroll size is really same height as the container?
I did override the two methods:
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
I can make:
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
Really high, but still, when scroll down to the bottom, it is not the last element.
try reducing the height of the table view frame. that should do the trick.
as a starter, try reducing it by half and then if everything works fine try to calculate the necessary height.
Check your table view's frame. If you have a navigation bar at the top your table view may be getting pushed down to accommodate it and your frame is too tall. Try reducing your table view's height.
If you have this. Remove this.
self.edgesForExtendedLayout = .None

Resources