UITableView insert rows without scrolling - iphone

I have a list of data that I'm pulling from a web service. I refresh the data and I want to insert the data in the table view above the current data, but I want to keep my current scroll position in the tableview.
Right now I accomplish this by inserting a section above my current section, but it actually inserts, scrolls up, and then I have to manually scroll down. I tried disabling scrolling on the table before this, but that didn't work either.
This looks choppy and seems hacky. What is a better way to do this?
[tableView beginUpdates];
[tableView insertSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
NSUInteger iContentOffset = 200; //height of inserted rows
[tableView setContentOffset:CGPointMake(0, iContentOffset)];

If I understand your mission correctly,
I did it in this way:
if(self.tableView.contentOffset.y > ONE_OR_X_ROWS_HEIGHT_YOUDECIDE
{
self.delayOffset = self.tableView.contentOffset;
self.delayOffset = CGPointMake(self.delayOffset.x, self.delayOffset.y+ insertedRowCount * ONE_ROW_HEIGHT);
[self.tableView reloadData];
[self.tableView setContentOffset:self.delayOffset animated:NO];
}else
{
[self.tableView insertRowsAtIndexPath:indexPathArray WithRowAnimation:UITableViewRowAnimationTop];
}
With this code, If user is in the middle of the table and not the top, the uitableview will reload the new rows without animation and no scrolling.
If user is on the top of the table, he will see row insert animation.
Just pay attention in the code, I'm assuming the row's height are equal, if not , just calculate the height of all the new rows you are going to insert.
Hope that helps.

The best way I found to get my desired behavior is to not animate the insertion at all. The animations were causing the choppyness.
Instead I am calling:
[tableView reloadData];
// set the content offset to the height of inserted rows
// (2 rows * 44 points = 88 in this example)
[tableView setContentOffset:CGPointMake(0, 88)];
This makes the reload appear at the same time as the content offset change.

For further spectators looking for a Swift 3+ solution:
You need to save the current offset of the UITableView, then reload and then set the offset back on the UITableView.
func reloadTableView(_ tableView: UITableView) {
let contentOffset = tableView.contentOffset
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.setContentOffset(contentOffset, animated: false)
}
Called by: reloadTableView(self.tableView)

Just call setContentOffset before endUpdates, that works for me.
[tableView setContentOffset:CGPointMake(0, iContentOffset)];
[tableView endUpdates];

I am using self sizing cells and the estimated row height was pretty useless because the cells can vary significantly in size. So calculating the contentOffset wasn't working for me.
The solution that I ended up with was quite simple and works perfectly.
So first up I should mention that I have some helper methods that allow me to get the data element for an index path, and the opposite - the index path for a data element.
-(void) loadMoreElements:(UIRefreshControl *) refreshControl {
NSIndexPath *topIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]
id topElement = [myModel elementAtIndexPath:topIndexPath];
// Somewhere here you'll need to tell your model to get more data
[self.tableView reloadData];
NSIndexPath *indexPath = [myModel indexPathForElement:topElement];
[self.tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
[refreshControl endRefreshing];
}

None of the answers here really worked for me, so I came up with my solution. The idea is that when you pull down to refresh a table view (or load it asynchronously with new data) the new cells should silently come and sit on top of the tableview without disturbing the user's current offset. So here goes a solution that works (pretty much, with a caveat)
var indexpathsToReload: [IndexPath] = [] //this should contain the new indexPaths
var height: CGFloat = 0.0
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
UIView.performWithoutAnimation {
self.tableview.reloadSections(NSIndexSet(index: 0) as IndexSet, with: .none)
self.tableview.layoutIfNeeded()
indexpathsToReload.forEach({ (idx) in
height += self.feed.rectForRow(at: idx).height
})
let afterContentOffset = self.tableview.contentOffset
let newContentOffset = CGPoint(x: afterContentOffset.x, y: afterContentOffset.y + height)
self.tableview.setContentOffset(newContentOffset, animated: false)
}
}
CAVEAT (WARNING)
This technique will not work if your tableview is not "full" i.e. it only has a couple of cells in it. In that case you would need to also increase the contentSize of the tableview along with the contentOffset. I will update this answer once I figure that one out.
EXPLANATION:
Basically, we need to set the contentOffset of the tableView to a position where it was before the reload. To do this we simply calculate the total height of all the new cells that were added using a pre populated indexPath array (can be prepared when you obtain the new data and add them to the datasource), these are the indexPaths for the new cells. We then use the total height of all these new cell using rectForRow(at: indexPath), and set that as the y position of the contentOffset of the tableView after the reload. The DispatchQueue.main.asyncAfter is not necessary but I put it there because I just need to give the tableview some time to bounce back to it's original position since I am doing it on a "pull to refresh" way. Also note that in my case the afterContentOffset.y value is always 0 so I could have hard coded 0 there instead.

Related

Animated UITableView reload causes every cell to load at once

I have a table view with 100 cells. At first, it is empty. Then, I call:
[_tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationLeft];
As there was nothing in the table view, I need to create several cells. However, I was expecting the table view to ask for 10 (to fit the screen size) cells... not 100 !
It doesn't happen when I simply reload the table view without any animation:
[_tableView reloadData];
This issue makes the table reloading very slow: is there a way to make it ask for 10 cells only ?
Edit
Maybe I wasn't clear enough: At first, the table has no entry. Then, I add 100 entries in my data source, and ask the table to reload: there is no visible cell before the reload, so the reloadRowsAtIndexPaths solution won't work.
It sounds like you're inserting new rows in the table, rather than reloading, so why not use:
[_tableView insertRowsAtIndexPaths:... withRowAnimation:...];
You may need to insert the section first:
[_tableView insertSections:... withRowAnimation:..];
http://developer.apple.com/library/ios/ipad/#documentation/uikit/reference/UITableView_Class/Reference/Reference.html
If you only want to reload lets say 10 cells, the following code will work.
int cellsToReload = 10;
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:cellsToReload];
for(int x = 0; x < cellsToReload; x++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:x inSection:0];
[indexPaths addObject:indexPath];
}
[self.theTableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationLeft];
In the above code when you reloaded all of the tableview, you were reloading the entire section and not just a few rows. I'm guessing your tableview only had one section.
NSArray *paths = [_tableView indexPathsForVisibleRows];
[_tableView reloadRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationLeft];
will do exactly what you need - reload only visible raws, neither more, nor less.
I found out that this issue occurs only on iOS 5.1 and below. No need to file a bug as it is corrected in iOS 6. Thank you for your answers anyway !

Animation looks awkward when deleting a cell whose height is greater than other cells

I have a plain UITableView with 4 rows, each with a height of 50. When I press the fourth row, I insert a fifth row with a height of 80 using UITableViewRowAnimationTop. So far, so good.
I want to delete the fifth row when the fourth row is pressed again. But when I delete the row using UITableViewRowAnimationTop (or any other animation style for that matter), the animation looks very awkward--the animation begins but the cell disappears abruptly before the animation is complete. (This is only apparent when you're using cells that have a background color. You can see the bottom half of the cell suddenly disappear rather than disappearing under the cell above it.)
The code is something like this:
[self.tableview beginupdates];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationTop];
[self.tableview endupdates];
The row heights are provided by the tableView:heightForRowAtIndexPath: method.
This problem does not occur when all rows have the same height, or when the row being deleted is shorter than the other rows.
I think one workaround is changing the height of the fifth cell to 50 before deleting it, but I'd rather not do that.Please help
I just ran into this problem this morning. Basically, I ended up handling the animation myself, then deleting the cell once the animation completed. Here's some code to show how I did that:
#property (nonatomic, strong) NSArray *tableData;
#property (nonatomic, strong) NSMutableDictionary *deletedItems;
...
- (void)deleteItemForCell:(UITableViewCell *)cell
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
NSObject *item = self.tableData[indexPath.row];
NSNumber *rowKey = #(indexPath.row);
__weak id weakSelf = self;
[CATransaction setCompletionBlock:^{
#synchronized(weakSelf) {
[self.deletedItems removeObjectForKey:rowKey];
[self.tableData removeObject:item];
[self.tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
}];
self.deletedItems[rowKey] = item;
[self.tableView beginUpdates];
[self.tableView endUpdates];
[CATransaction commit];
}
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (self.deletedItem[#(indexPath.row)]) {
return 0;
}
return CELL_HEIGHT;
}
The code tracks deleted items in the data structure and informs the table to update. Any items in the deleted data structure will get a height of 0, which will result in a "collapse" animation.
The code utilizes a CATransaction to know when the animation has completed. Once it's completed the item is deleted from the tableData data structure and deleted from the table, without any animation.
NOTE:
I'm not sure if the synchronized is required, but seemed smart to have, since we're dealing with asynchronously deleting items.
I tried using the indexPath object as the key for the dictionary, but it didn't seem to work. Used the row instead.
Hope this helps.
Using a UITableViewRowAnimationMiddle made it slightly less terrible for me.
Similarly, I noticed that with UITableViewRowAnimationTop, the height beyond the tableviews default cell height appears/disappears without animation. So if your tableview row height is 44, but you animate in a cell with height of 74, it will immediately show 30, and then animate in the remaining 44.
With UITableViewRowAnimationMiddle the cell content animates in nicely, but the cell border is glitchy for the duration of the animation - originally appearing at 30, then animating the remaining 44.
Have you tried doing it without begin/endUpdates? I vaguely recall using those making things weird once.
Have you tried doing a reloadRowsAtIndexPaths: for the next row? Presuming there is one, that is.
Are you sure the data source is behaving correctly in terms of heightForRow response at the point where you make the deleteRowsAt call? (e.g., you may set some flag to indicate the thing is not visible anymore after the delete call which may result in the new row having a bad height)

Why do my table view cells disappear when reloaded using reloadRowsAtIndexPaths?

I have a bare-bones sample project here:
http://dl.dropbox.com/u/7834263/ExpandingCells.zip
In this project, a UITableView has a custom UITableViewCell. In each cell are 3 UIViews containing a label.
The goal is to expand the cell when tapped, then collapse it when tapped again. The cell itself must change it’s height to expose the subviews. Inserting or removing rows is unacceptable.
The demo project works almost as expected. In fact, in iOS 4.3 it works perfect. Under iOS 5, however, when the rows collapse, the previous cells magically disappear.
To re-create the problem, run the project in the simulator or device with iOS 5 and tap the first cell to expand it. Then tap the cell again to collapse it. Finally, tap the cell directly underneath it. The previous one disappears.
Continuing the tapping for each cell in the section will cause all the cells to disappear, to where the entire section is missing.
I’ve also tried using reloadData instead of the current setup, but that ruins the animations and feels a bit like a hack anyway. reloadRowsAtIndexPaths should work, but the question is why doesn’t it?
See images of what's happening below:
Table appears:
Cell expands:
Cell collapses:
Cell disappears (when tapping the cell underneath):
Keep repeating until the entire section disappears:
EDIT:
Overriding the alpha is a hack, but works. Here is another 'hack' that fixes it as well but WHY does it fix it?
JVViewController.m line 125:
if( previousIndexPath_ != nil )
{
if( [previousIndexPath_ compare:indexPath] == NSOrderedSame ) currentCellSameAsPreviousCell = YES;
JVCell *previousCell = (JVCell*)[self cellForIndexPath:previousIndexPath_];
BOOL expanded = [previousCell expanded];
if( expanded )
{
[previousCell setExpanded:NO];
[indicesToReload addObject:[previousIndexPath_ copy]];
}
else if( currentCellSameAsPreviousCell )
{
[previousCell setExpanded:YES];
[indicesToReload addObject:[previousIndexPath_ copy]];
}
//[indicesToReload addObject:[previousIndexPath_ copy]];
}
EDIT 2:
Made a few minor changes to demo project, worth checking out and reviewing JVViewController didSelectRowAtIndexPath method.
Your problem is in setExpanded: in JVCell.m, you are directly editing the frame of the target cell in that method.
- (void)setExpanded:(BOOL)expanded
{
expanded_ = expanded;
CGFloat newHeight = heightCollapsed_;
if( expanded_ ) newHeight = heightExpanded_;
CGRect frame = self.frame;
frame.size.height = newHeight;
self.frame = frame;
}
Update it to:
- (void)setExpanded:(BOOL)expanded
{
expanded_ = expanded;
}
Then remove the call to -reloadRowsAtIndexPaths:withRowAnimation: at line 163 of JVViewController.m and it will animate as expected.
-reloadRowsAtIndexPaths:withRowAnimation: expects different cells to be returned for the provided indexPaths. Since you are only adjusting sizes -beginUpdates & -endUpdates is sufficient to layout the table view cells again.
May be I am missing a point, but why dont you just use:
UITableViewRowAnimationNone
I mean instead of :
[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationAutomatic];
use
[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationNone];
To animate the height changes of a tableView just call.
[tableView beginUpdates];
[tableView endUpdates];
Don't call reloadRowsAtIndexPaths:
See Can you animate a height change on a UITableViewCell when selected?
The cell that is fading out is the previous cell that is not changing size. As the documentation of reloadRowsAtIndexPaths:withRowAnimation: states:
The table animates that new cell in as it animates the old row out.
What happens is the opacity is set to 1 then immediately set to 0 and so it fades out.
If both the previous and the new cell change size then it works as intended. This is because the begin/end Updates notice the height changes and create new animations on those cells overriding the reloadRowsAtIndexPaths:withRowAnimation: ones.
Your problem is due to abusing reloadRowsAtIndexPaths:withRowAnimation: to resize the cells when it's intended for loading new cells.
But you don't need reloadRowsAtIndexPaths:withRowAnimation: at all. Just change the expanded state of the cells and do the begin/end updates. That will handle all the animation for you.
As a side note I found the blue selection a little annoying, in JVCell set the selectedBackgroundView to the same image as the backgroundView (or create a new image that has the correct look of a selected cell).
EDIT:
Move the statement adding previousIndexPath_ to indicesToReload to the if statement (at line 132) so that it is only added if the previous cell was expanded and needs to resize.
if( expanded ) {
[previousCell setExpanded:NO];
[indicesToReload addObject:[previousIndexPath_ copy]];
}
This removes the case where the previous collapsed cell would disappear.
Another option would be to set previousIndexPath_ to nil when the current cell is collapsed and only set it when a cell expands.
This still feels like a hack. Doing both the reloadRows and the begin/end Updates causes the tableView to reload everything twice but both seem to be needed to animate correctly. I suppose if the table is not too large this won't be a performance problem.
Short, pragmatic answer: Changing UITableViewRowAnimationAutomatic to UITableViewRowAnimationTop solves the issue. No more disappearing rows! (tested on iOS 5.1)
Another short, pragmatic answer, since UITableViewRowAnimationTop is said to cause its own issues: Create a new cell view instead of modifying the existing one's frame. In a real app the data displayed in the cell view is supposed to be in the Model part of the app anyway, so if properly designed it shouldn't be a problem to create another cell view which displays the same data only in a different manner (frame in our case).
Some more thoughts regarding animating the reload of the same cell:
UITableViewRowAnimationAutomatic seems to resolve to UITableViewRowAnimationFade in some cases, which is when you see the cells fading away and disappearing. The new cell is supposed to fade in while the old one fades out. But here the old cell and the new one are one and the same - So, could this even work? In the Core Animation level, Is it possible to fade out a view AND fade it in at the same time? Sounds dubious. So the result is that you just see the fade out. This could be considered an Apple bug, since an expected behavior could be that if the same view has changed, the alpha property wouldn't be animated (since it can't animate both to 0 and to 1 at the same time), but instead just the frame, color etc. would be animated.
Note the problem is just in the animation's display - if you scroll away and back, everything will appear correctly.
In iOS 4.3 the Automatic mode might have been resolved to something other than Fade which is why things work there (as you write they do) - I didn't dig into that.
I don't know why iOS chooses the Fade mode when it does. But one of the cases it does is when your code asks reloads a previously tapped cell, which is collapsed, and is different than the current tapped cell. Note the previously tapped cell is always reloaded, this line in your code is always called:
[indicesToReload addObject:[previousIndexPath_ copy]];
This explains the magic disappearing cells scenario you have described.
By the way, the beginUpdates/endUpdates seem like a hack to me. This pair of calls is just supposed to contain animations, and there aren't any animations you are adding in addition to the rows you already asked to reload. All it did in this case is magically cause the Automatic mode to not choose Fade in some cases - But this just obscured the problem.
A final note: I played around with the Top mode and found it can also cause problems. For example plugging the following code makes cells disappear funkily:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationTop];
}
Not sure if there is a real issue here (similar to the one with fading a view in and out at the same time), or maybe an Apple bug.
I just downloaded your project & found this section of code in didSelectRowAtIndexPath delegate where reloadRowsAtIndexPaths is used.
[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView beginUpdates];
[tableView endUpdates];
instead of the above why don't you try this?
[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
The reason i am suggesting this is that I believe reloadRowsAtIndexPaths:... only works when wrapped inbetween calls to:
- (void)beginUpdates;
- (void)endUpdates;
Outside of that, behavior is undefined and as you've discovered, fairly unreliable. Quoting relevant part of "Table View Programming Guide for iPhone OS":
To animate a batch insertion and deletion of rows and sections, call the insertion and deletion methods within an animation block defined by successive calls to beginUpdates and endUpdates. If you don’t call the insertion and deletion methods within this block, row and section indexes may be invalid. beginUpdates...endUpdates blocks are not nestable.
At the conclusion of a block—that is, after endUpdates returns—the table view queries its data source and delegate as usual for row and section data. Thus the collection objects backing the table view should be updated to reflect the new or removed rows or sections.
The reloadSections:withRowAnimation: and reloadRowsAtIndexPaths:withRowAnimation: methods, which were introduced in iPhone OS 3.0, are related to the methods discussed above. They allow you to request the table view to reload the data for specific sections and rows instead of loading the entire visible table view by calling reloadData.
There could be other valid reason but let me mull on this a bit, since i have your code too I could muck around with it. Hopefully we should figure it out...
When you use this method, you have to be sure that you are on the main thread.
Refreshing a UITableViewCell as follow should do the trick :
- (void) refreshTableViewCell:(NSNumber *)row
{
if (![[NSThread currentThread] isMainThread])
{
[self performSelector:_cmd onThread:[NSThread mainThread] withObject:row waitUntilDone:NO];
return;
}
/*Refresh your cell here
...
*/
}
I think you should not retain the previous indexPath when the cell is not expanded,
try by modifying you did select method like the below, its working fine..
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
BOOL currentCellSameAsPreviousCell = NO;
NSMutableArray *indicesToReload = [NSMutableArray array];
if(previousIndexPath_ != nil)
{
if( [previousIndexPath_ compare:indexPath] == NSOrderedSame ) currentCellSameAsPreviousCell = YES;
JVCell *previousCell = (JVCell*)[self cellForIndexPath:previousIndexPath_];
BOOL expanded = [previousCell expanded];
if(expanded)
{
[previousCell setExpanded:NO];
}
else if (currentCellSameAsPreviousCell)
{
[previousCell setExpanded:YES];
}
[indicesToReload addObject:[previousIndexPath_ copy]];
if (expanded)
previousIndexPath_ = nil;
else
previousIndexPath_ = [indexPath copy];
}
if(currentCellSameAsPreviousCell == NO)
{
JVCell *currentCell = (JVCell*)[self cellForIndexPath:indexPath];
BOOL expanded = [currentCell expanded];
if(expanded)
{
[currentCell setExpanded:NO];
previousIndexPath_ = nil;
}
else
{
[currentCell setExpanded:YES];
previousIndexPath_ = [indexPath copy];
}
// moving this line to inside the if statement blocks above instead of outside the loop works, but why?
[indicesToReload addObject:[indexPath copy]];
}
// commenting out this line makes the animations work, but the table view background is visible between the cells
[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationAutomatic];
// using reloadData completely ruins the animations
[tableView beginUpdates];
[tableView endUpdates];
}
This problem is caused by returning cached cells in cellForRowAtIndexPath.
The reloadRowsAtIndexPaths is expecting to get fresh new cells from cellForRowAtIndexPath.
If you do that you will be ok ... no workarounds required.
From Apple doc:
"Reloading a row causes the table view to ask its data source for a new cell for that row."
I had a similar issue where I wanted to expand a cell when a switch is activated to display and extra label and button in the cell that is normally hidden when the cell is at its default height (44). I tried various versions of reloadRowsAtPath to no avail. Finally I decided to keep it simpler by adding a condition at heightForRowAtIndexPath like so:
override func tableView(tableView: UITableView,heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if ( indexPath.row == 2){
resetIndexPath.append(indexPath)
if resetPassword.on {
// whatever height you want to set the row to
return 125
}
}
return 44
}
And in whatever code you want to trigger the expanding of the cell just insert tableview.reloadData(). In my case it was when a switch was turned on to indicate the desire to reset a password.
#IBAction func resetPasswordSwitch(sender: AnyObject) {
tableView.reloadData()
}
There is no lag with this approach, no visible way to see that the table reloaded and the expansion of the sell is done gradually like you'd expect. Hope this helps someone.
#Javy, i noticed some weird behavior while testing your app.
While running on iPhone 5.0 simulator the previousIndexpth_ variable is of
class NSArray (it looks like.) here is the debugger output
(lldb) po previousIndexPath_
(NSIndexPath *) $5 = 0x06a67bf0 <__NSArrayI 0x6a67bf0>(
<JVSectionData: 0x6a61230>,
<JVSectionData: 0x6a64920>,
<JVSectionData: 0x6a66260>
)
(lldb) po [previousIndexPath_ class]
(id) $7 = 0x0145cb64 __NSArrayI
Whereas in iPhone 4.3 simulator it is of type NSIndexPath.
lldb) po [previousIndexPath_ class]
(id) $5 = 0x009605c8 NSIndexPath
(lldb) po previousIndexPath_
(NSIndexPath *) $6 = 0x04b5a570 <NSIndexPath 0x4b5a570> 2 indexes [0, 0]
Are you aware of this issue? Not sure whether this will help but thought of letting you know.
Try this hope it will help u Cell Expansion

UITableView scroll to bottom to see insertion animation

I have a UITableView that I am adding a row to with an animation (using insertRowsAtIndexPaths:withRowAnimation:). This is all good as long as the table is not longer than the screen.
If it is bigger than the screen then I am trying to scroll to the bottom, but it is not quite working how I want. If I scroll to the new row after it is added I miss the animation. If I try to scroll to that indexPath before it is added it throws an exception (about it not being a valid indexPath)
Is there a solution to this other than adding a blank row?
I had this same issue. Here was my solution.
First -
Update your data source
Second -
NSIndexPath *path = [NSIndexPath indexPathForRow:([arrayWhichFeeds count] - 1 inSection:0]];
NSArray *paths = [NSArray arrayWithObject:path];
[myTableView insertRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationTop];
[myTableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionBottom animated:YES];
*Please note that this IS NOT wrapped in the [tableView beginUpdades] and [tableView endUpdates] code. If you do it will not work as desired.
Give it a try, it should animate in the new rows from the bottom while scrolling to them.
Yes there is a solution without adding a blank row.
Note: In the code, I consider that there is only 1 section but many rows. You can modify the code to manage multiple sections as well.
- (void)theMethodInWhichYouInsertARowInTheTableView
{
//Add the object in the array which feeds the tableview
NSString *newStringObject = #"New Object";
[arrayWhichFeeds addObject:newStringObject];
[myTableView beginUpdates];
NSArray *paths = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:([arrayWhichFeeds count] - 1) inSection:0]];
[myTableView insertRowsAtIndexPaths:paths withRowAnimation:NO];
[myTableView endUpdates];
[myTableView reloadData];
[myTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:([arrayWhichFeeds count] - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
Other techniques, including those mentioned under this question did not work for me. This did however:
Add the new item to the internal dataSource collection.
Set a flag in the item that indicates it is "new". Force the display of the cell to show nothing when this flag is set.
Immediately call tableView:reloadData:
Now the new item is in this table but will visually appear empty (due to the flag).
Check to see if this new item is visible using tableView.indexPathsForVisibleRows.
If the item was is on screen immediately flip that "new" flag on the dataSource item to NO and call tableView:reloadRowsAtIndexPaths with an animation set. Now it will appear as if this item was just added. (you're done in this case)
If it wasn't on screen scroll it into view with tableView:scrollToRowAtIndexPath: but don't immediately call reloadRowsAtIndexPath...
Handle the message (void)scrollViewDidEndScrollingAnimation: and do that same reloadRowsAtIndexPath from step 6. I suspect this method is called anytime scrolling happens, so you'll have to detect when it's called from step 7 and when it's called because the user is scrolling around.
I worked this technique out (and wrote this answer) when I 1st started iOS development, but this did work out long term.
Remeber to do this in the main thread otherwise it won't scroll to the required position.
Update data source
Insert row at bottom
Scroll to bottom
Example:
YOUR_ARRAY.append("new string")
tableView.insertRows(at: [IndexPath(row: YOUR_ARRAY.count-1, section: 0)], with: .automatic)
tableView.scrollToRow(at: IndexPath(row: YOUR_ARRAY.count-1, section: 0), at: UITableViewScrollPosition.bottom, animated: true)
More generally, to scroll to the bottom:
NSIndexPath *scrollIndexPath = [NSIndexPath indexPathForRow:([self.table numberOfRowsInSection:([self.table numberOfSections] - 1)] - 1) inSection:([self.table numberOfSections] - 1)];
[self.table scrollToRowAtIndexPath:scrollIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
This works for me. Instead of inserting a row I just highlight it (by fading it in out.) The principle is the same, though.
if let definiteIndexPath = indexPathDelegate.getIndexPath(toDoItem) {
UIView.animateWithDuration(0.5, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: {
self.tableView.scrollToRowAtIndexPath(definiteIndexPath, atScrollPosition: .Middle, animated: false)
}, completion: {
(finished: Bool) -> Void in
// Once the scrolling is done fade the text out and back in.
UIView.animateWithDuration(5, delay: 1, options: .Repeat | .Autoreverse, animations: {
self.tableView.reloadRowsAtIndexPaths([definiteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}, completion: nil)
})
}

How do I scroll a UITableView to a section that contains no rows?

In an app I'm working on, I have a plain style UITableView that can contain a section containing zero rows. I want to be able to scroll to this section using scrollToRowAtIndexPath:atScrollPosition:animated: but I get an error when I try to scroll to this section due to the lack of child rows.
Apple's calendar application is able to do this, if you look at your calendar in list view, and there are no events in your calendar for today, an empty section is inserted for today and you can scroll to it using the Today button in the toolbar at the bottom of the screen. As far as I can tell Apple may be using a customized UITableView, or they're using a private API...
The only workaround I can think of is to insert an empty UITableCell in that's 0 pixels high and scroll to that. But it's my understanding that having cells of varying heights is really bad for scrolling performance. Still I'll try it anyway, maybe the performance hit won't be too bad.
Update
Since there seems to be no solution to this, I've filed a bug report with apple. If this affects you too, file a duplicate of rdar://problem/6263339 (Open Radar link) if you want this to get this fixed faster.
Update #2
I have a decent workaround to this issue, take a look at my answer below.
UPDATE: Looks like this bug is fixed in iOS 3.0. You can use the following NSIndexPath to scroll to a section containing 0 rows:
[NSIndexPath indexPathForRow:NSNotFound inSection:section]
I'll leave my original workaround here for anyone still maintaining a project using the 2.x SDK.
Found a decent workaround:
CGRect sectionRect = [tableView rectForSection:indexOfSectionToScrollTo];
[tableView scrollRectToVisible:sectionRect animated:YES];
The code above will scroll the tableview so the desired section is visible but not necessarily at the top or bottom of the visible area. If you want to scroll so the section is at the top do this:
CGRect sectionRect = [tableView rectForSection:indexOfSectionToScrollTo];
sectionRect.size.height = tableView.frame.size.height;
[tableView scrollRectToVisible:sectionRect animated:YES];
Modify sectionRect as desired to scroll the desired section to the bottom or middle of the visible area.
If your section have not rows use this
let indexPath = IndexPath(row: NSNotFound, section: section)
tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
This is an old question, but Apple still haven't added anything which helps or fixed the crash bug where the section has no rows.
For me, I really needed to make a new section scroll to the middle when added, so I now use this code:
if (rowCount > 0) {
[self.tableView scrollToRowAtIndexPath: [NSIndexPath indexPathForRow: 0 inSection: sectionIndexForNewFolder]
atScrollPosition: UITableViewScrollPositionMiddle
animated: TRUE];
} else {
CGRect sectionRect = [self.tableView rectForSection: sectionIndexForNewFolder];
// Try to get a full-height rect which is centred on the sectionRect
// This produces a very similar effect to UITableViewScrollPositionMiddle.
CGFloat extraHeightToAdd = sectionRect.size.height - self.tableView.frame.size.height;
sectionRect.origin.y -= extraHeightToAdd * 0.5f;
sectionRect.size.height += extraHeightToAdd;
[self.tableView scrollRectToVisible:sectionRect animated:YES];
}
Hope you like it - it's based on Mike Akers' code as you can see, but does the calculation for scrolling to the middle instead of top. Thanks Mike - you're a star.
A Swift approach to the same:
if rows > 0 {
let indexPath = IndexPath(row: 0, section: section)
self.tableView.setContentOffset(CGPoint.zero, animated: true)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
else {
let sectionRect : CGRect = tableView.rect(forSection: section)
tableView.scrollRectToVisible(sectionRect, animated: true)
}
Since using:
[NSIndexPath indexPathForRow:NSNotFound inSection:EXAMPLE]
Broken for me in Xcode 11.3.1 (iOS simulator - 13.3) I decided to use:
NSUInteger index = [self.sectionTypes indexOfObject:#(EXAMPLE)];
if (index != NSNotFound) {
CGRect rect = [self.tableView rectForSection:index];
[self.tableView scrollRectToVisible:rect animated:YES];
}
I think a blank row is probably the only way to go there. Is it possible to redesign the UI such that the "empty" row can display something useful?
I say try it out, and see what the performance is like. They give pretty dire warnings about using transparent sub-views in your list items, and I didn't find that it mattered all that much in my application.