UITableView bouncing back to the top of a section when calling reloadRowsAtIndexPaths - ios

When a user taps a button in one of my rows I am updating the underlying model for that row and then calling reloadRowsAtIndexPaths for the given row (i.e. single row reload).
- (IBAction)handleCompleteTouchEvent:(UIButton *)sender {
NSIndexPath *indexPath = [self.tableView indexPathForView:sender];
id item = [self dataForIndexPath:indexPath];
if ([item respondsToSelector:#selector(completed)]) {
// toogle completed value
BOOL completed = ![[item valueForKey:#"completed"] boolValue];
[item setValue:[NSNumber numberWithBool:completed] forKey:#"completed"];
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
}
}
The problem is that the table view bounces back to the top of the section after making this call. How can I prevent this from occurring and keep the scroll position where it is?

Ah Ha! I found the problem and am going to answer my own question for the poor soul who runs into this issue in the future.
All of my cells have variable height so I was using the new iOS7 method in UITableViewDelegate thinking it might speed up render time (not that I really needed it):
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath;
Anyway, implementing this method has the evil side effect of causing the table to bounce to the top of the section when calling:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
To solve the bounce problem I just removed the override of that estimatedHeightForRowAtIndexPath method and now everything works as it should. Happy at last.

This did the trick for me.
UIView.setAnimationsEnabled(false)
tableView.reloadRows(at: [...], with: .none)
UIView.setAnimationsEnabled(true)

Swift 4.2
This can work anywhere you want to remove animation.
During reload of table , table section or any row
UIView.performWithoutAnimation({
cell.configureSelection(isSelected: true)
tableView.reloadSections([1], with: .none)
tableView.allowsSelection = false
})

You should be able to do what you are trying to do by changing the cell contents directly. For example, if you are using the base UITableViewCell class and the data in your model is a NSString which you show in the table view cell, you can do the following (after you change your data model) instead of calling reloadRowsAtIndexPaths:
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
UILabel *label = [cell textLabel];
label.text = #"New Value";
If you are using a custom subclass of UITableViewCell, it's roughly the same except for accessing the views of the cell will need to be done through the contentView property.

It may be possible to put the cell in it's own section and call reload section:
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
this appears to fix the issue partially. Not the cleanest way but it may work for you.

In my similar case, I had to tweak the implementation of method
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
...
}
where my heights were being reset. So, instead of resetting height for every cell I updated only the effected cells for reloadRowsAtIndexPaths call.

If you know the minimum height of your cell, you must specify that while setting estimatedRowHeight. I was setting it to 1 as I have read before somewhere that any value above 0 would suffice the purpose, but it was the culprit.
When I set it to 44, which was the minimum height my cell could have, all went fine.
Dynamic heights were also working fine, no issues with this fix.

To build off xsee's answer -
I had set the Estimate in interface builder to "automatic". I changed this to another number and it started working. I kept Row Height to automatic.

I had the same issue. I ended up just calling tableView.reloadData() instead after updating my data / cell and it didn't bounce back to the top / data was updated in place - FYI

This solved this issue for me with Swift 5 / iOS 14:
UIView.performWithoutAnimation({
self.tableView.reloadRows(at: [self.idxTouched], with: .none)
})

Related

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;
}

Cell overlaps and shows full content while editing

I am using cells with two heights based on some status.
When start is StatusAwarded it shows full cell else it will show a part of it.
Here is the code to achieve this.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
SomeClass *someClass = dataArray[indexPath.row];
if (someClass.status == StatusAwarded) {
return 252.0f;
}
return 110.0f;
}
It works all fine but when it is in edit mode, it has this issue as shown below.
I am having effect like this while editing (Deleting).
Question:
Why is this happening? How to fix this?
Note: Clip Subviews is set to YES
Try to enable Clip Subviews in Interface Builder / Storyboard.
You're not reloading the other cells, try:
[tableViewAdvancedBookings reloadRowsAtIndexPaths:[indexPathsToExpand allObjects] withRowAnimation:UITableViewRowAnimationAutomatic];
instead of:
[tableViewAdvancedBookings reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
which only reload the currect selected cell..
or maybe try reloading the whole table like:
[tableView reloadData];
and see what happen.. hope i've help, for some reason.. Good night, Cheers..

UITableViewCell being reused while still on-screen?

I have what probably seems like a really weird problem (it does to me!)
I am using a UITableView to display cells which each contain a UIWebView. I realise that this is a bad idea on the face of it, but I can't really do this any other way.
I am caching the heights of each cell when the UIWebView finishes loading, and then calling:
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[cellIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
All of the germane code is in a Gist here.
I also have the UIWebViews cached in a dictionary on the data source, so it can be reused when the cell is reloaded.
This seems to sort of work, but I am encountering a lot of issues whereby the cells' contents will randomly disappear. I have added some logging into determine what's going on, and in what order, and it seems like some of the cells are being reused while they're still on-screen.
I see this in my logs while scrolling down:
2014-02-11 13:45:49.091 EApp[45936:70b] Generating cell for 1: Panning
2014-02-11 13:45:49.245 EApp[45936:70b] Generating cell for 2: Calibration
2014-02-11 13:45:50.063 EApp[45936:70b] Generating cell for 3: Aperture Priority
2014-02-11 13:45:50.063 EApp[45936:70b] Reusing cell: Stopping down
"Stopping down" in this case is a cell that is still on-screen. The "generating cell" items are logged inside the data source's cellForRowAtIndexPath and the "reusing" messages inside the cells' prepareForReuse.
Does anyone know what could be happening here? I know this seems complex.
The following line in your prepareForReuse is probably the culprit:
if ([self.contentWebView isDescendantOfView:self.contentWebView]) {
[self.contentWebView removeFromSuperview];
}
As the contentWebView is never a descendant of itself, it will not be removed from the cell, and the contentView will contain two webviews after the cellForRowAtIndexPath:
You probably meant to say:
if ([self.contentWebView isDescendantOfView:self.contentView]) {
[self.contentWebView removeFromSuperview];
}
Or simply:
[self.contentWebView removeFromSuperview];
One of the features/limitations of UITableView is that you don't know if, and can't depend on, a cell is being created or reused. You should always be able to handle both.
GENERALLY, when you call -reloadRowsAtIndexPaths:withRowAnimation:, you will get the cell from that indexPath to reuse. If that indexPath was on screen, it will be a cell that was on screen.
I don't know if it's the problem, but in the code you provided, you don't even initialize your cell...
I'm even surprise it works.
static NSString *CellIdentifier = #"FeedItemCell";
EFeedItemCell *cell = [self.tableViewController.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
you should add to it :
if (!cell) {
cell = [EfeedItemCell alloc] initWithReus....];
}
From your code it seems that you are caching the webViews and then are adding them to cells programmatically. This can create random problems similar to what I had faced in the passed.
You must use EFeedItemCellWebView in your storyboard. Just add a UIWebView and change the class name to your custom class. And then when the data is loaded just simply change its contents in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

deleteRowsAtIndexPaths Overlap Issue

Trying to be ios7-esque, I am inserting a UIPickerView into a UITableView when tapping on a cell in the table. This works fine and animates nicely. However, there is an issue when I retract the cell by calling deleteRowsAtIndexPaths.
I am experiencing a "bleed"/overlap where the picker is hiding one of the cells further down in the table view. See the screenshots.
I'm not doing anything super custom, so I wonder if this is an iOS7 bug. All cells have solid background colors (white).
Any help would be greatly appreciated. Thanks!
Tapping the top row
This is mid animation when retracting. Notice the overlap and the picker bleeding out over the cell at the bottom
I'm not sure why, but it looks to me like the picker cell is covering the cell below "Choose Product". If this is indeed the case, one workaround would be to explicitly set the z-order of your cells, placing the picker cell under all others:
#import <QuartzCore/QuartzCore.h>
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = ...;// your logic for getting a cell
BOOL isPickerCell = ...;// your logic for identifying if this is the picker cell
cell.layer.zPosition = isPickerCell ? 0 : 1;
}
If the picker is near the bottom of the table, it could still show through below the last cell since there's nothing there to cover it. For example, if "Choose Product" were the last cell. You can work around this by inserting blank cell(s) at the bottom. This is a general problem with having cells of varying height.
After struggling with this problem, I realised that Apple's calendar application has the same issue
However, they minimise the side effects by inserting the row with a .fade animation.
tableView.insertRows(at: indexes, with: .fade)
I had similar issues (iOS 8, iPhone 6 simulator)
In my case, I had a custom cell containing a DatePicker being inserted/deleted either between Right Detail style cells or between a Right Detail style cell and the section footer, which worked as expected.
[self.table beginUpdates];
if (isVisible) {
[self.table insertRowsAtIndexPaths:#[index]
withRowAnimation:UITableViewRowAnimationMiddle];
} else {
[self.table deleteRowsAtIndexPaths:#[index]
withRowAnimation:UITableViewRowAnimationMiddle];
}
[self.table endUpdates];
But I also had Right Detail style cell being inserted/deleted between a Right Detail style cell and the end of section, which did not work as expected using the same code. The appearing/disappearing cell was visible on top of/through the cell above, and the cell moved twice as far as it should have. In the image below, People is appearing below Privacy, mid-animation.
However, I noticed that when the beginUpdates/endUpdates were commented out, the cell only moved about half a cell height instead of twice a cell height which meant that it looked much improved.
I also tried setting the zPosition which appeared to lessen the visibility when the cells overlapped.
// [self.table beginUpdates];
if (isVisible) {
[self.table insertRowsAtIndexPaths:#[index]
withRowAnimation:UITableViewRowAnimationMiddle];
} else {
cell = [self.table cellForRowAtIndexPath:peopleIndex];
cell.layer.zPosition = -1;
[self.table deleteRowsAtIndexPaths:#[peopleIndex]
withRowAnimation:UITableViewRowAnimationMiddle];
}
// [self.table endUpdates];

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.

Resources