UITableView dynamic content loading when scrolling - ios

I'm trying to implement an infinite UITableView. I load some information from the server in chunks and I want to dynamically add this information to the tableview when user scrolls the table.The problem I'm facing : I have an initial tableview with 40 elements and then in scrollViewDidScroll I check if last visible row's indexPath == [myData count] - 20 (so when user has already scrolled half of the list I load next content) I load next 40 elements of content from the server and only when last visible row's indexPath == [myData count] -1 I actually reload the tableview. So as you see i make some eager caching in order to make tableview reloading faster.Everything work perfectly but when I begin to scroll my table really fast data stopped loading. It seems that this check: last visible row's indexPath == [myData count] - 20 has actually no chance to implement because of fast scrolling.I can't see any other obvious reasons for this. So has someone some answer for this weird problem?
P.S. Or may be I should try reloadRowsAtIndexPaths instead of reloading the whole table?

Maybe with this delegate you have a chance to do something in advance. There is also possible to reload only the visible rows which it's better than reload data.
-(void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
NSArray *paths = [self.tableView indexPathsForVisibleRows];
int tmprow = [[paths objectAtIndex:0] row];
}

Related

UITableView Load more autosrolling issue

Folks,
I am getting a weird issue while applying scroll to load more to my UITableView. I added a YouTube video also for reference. At scrolling to bottom, API hits server and gets next 5 records. At this time I am not reloading the table, neither I want too. I am simply adding more rows to the table. The code is below:
// adding new rows rather than reloading the table
if(loadMore)
{
NSMutableArray *indexPaths = [NSMutableArray array];
for (NSUInteger i = 0; i < arr.count; i++)
{
[indexPaths addObject:[NSIndexPath indexPathForRow:arr.count+i inSection:0]]; // adding max 5 new indexPaths
}
[self.tblData beginUpdates];
[self.tblData insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; // insert with fade animation
[arrData addObjectsFromArray:arr]; // adding new data to existing array
[self.tblData endUpdates];
}
Now see the video I uploaded. On 1st load more it works like charm. You can see it at 00:07. It adds 5 more rows at the bottom with fade animation.
But after 1st load more, for next 5 load more it brings 5 more records from the server and adds them to the table but also it autoscrolls at the same index, see video at 00:15, 00:23, 00:30 an so on.
You can see it works good on 1st load more (at 00:07), but never after that even all the records are coming good.
After that I changed simulator animation to Slow and you can see that after 01:01
Can anyone suggest me why it is happening? Somewhere I found it is due to AutoLayout, then i made another storyboard which has disabled autolayout but problem still occurs.

Load cells in UITableView

I have UITableView with cells, i use footer(height = 10pt) for each sections, header (height = 64pt) for each sections and cell (hight = self.view.frame.size.hight - 64 - 10).
TableView loads only first cell, when i launch app (because second cell is not visible on screen), but i want to load first and next cell. And when i scroll to second row i want to load third cell.
How can i achieve this?
UITableView delegate methods provide you several ways to track which cell just appeared on screen (i.e. tableView:willDisplayCell:forRowAtIndexPath:).
One way to do it is to track which cell was displayed, and load +1 and/or -1 indexes manually.
Note: you shouldn't load the UITableViewCells directly but the content they are supposed to show (according to M.V.C).
lazy-loading suit for you, which means first is visible, when scroll down second item should be ready.
//Store all items
NSArray *allItems = #[].mutableCopy;
//Load first two and store into allItems
NSDate *first, *second;
allItems = #[first,second];
//Check which cell is visible when scroll down and if the data has been ready
1) path.section is the visible cell
NSIndexPath *path = [tableView indexPathForCell:aCell];
2) allItems count is the number of data you have loaded
allItems.count = path.section + 1;
3) If allItems.count == path.section, you should load the next data, which is next of the current visible one.
By these two conditions the data should be ready all the time.
After each load [tableView reload] to display.

scrollToRowAtIndexPath doesn't scroll to correct position when segment value changed

I have a segment control with a table view. When user clicks segment control, then table view changes data source. I would like table to scroll to last position user had seen.
For example: user was in segment 1 and looking at table view line 3.
segment 1 | segment 2
line 3
line 4
line 5
Then user clicked segment 2 then clicked back to segment 1. I want table view scroll to line 3.
Code is as below. In viewDidAppear I have same code to get lastSavedRow and call scrollToRowAtIndexPath. When app gets loaded the scrolling works. However, in segmentValueChanged action it doesn't roll to correct position. In debugger I am sure the lastSavedRow has correct number.
- (IBAction) segmentVauleChanged : (id)sender
{
// remember row number before segment value changes
NSArray *indexPaths = [self.tableView indexPathsForVisibleRows];
NSArray * sortedIndexPaths = [indexPaths sortedArrayUsingSelector:#selector(compare:)];
NSIndexPath *firstVisibleIndexPath = [sortedIndexPaths objectAtIndex:0];
// function to write row number and seg index to user default
[self saveScrollPositionInSegment: self.currentSegmentSelectedIndex row:firstVisibleIndexPath.row];
self.currentSegmentSelectedIndex = segmentController.selectedSegmentIndex;
[self.tableView reloadData];
// scroll to last saved position
int lastSavedRow = [self getScrollPositionInSegment:segmentController.selectedSegmentIndex];
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:lastSavedRow inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
Does anyone have same problem? Many thanks in advance!
The lastSavedRow will on the top of the current tableview. I think there maybe something wrong with the dealing with datasource. Maybe after change the datasource, the last postion the user seen was not the index you figured out!
If you provide more information or your example, we can help you to find out the mistake.
I would do the following.
1 .Call visibleCells before calling indexPathsForVisibleRows
[self.tableView visibleCells];
2.Look at the need for sorting the array before identifying firstVisibleIndexPath
3.reloadData is not synchronous so its possible that the table is not completed loading before you try to scroll to an index. So try to see if you can do the scrolling in one of the delegate functions instead of here. See this.
hackerinheels 's (3) is right. reloadData hasn't finished when scrolling table is called. So I added the code:
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
if (section == ([self.tableView numberOfSections] - 1)) {
//NSLog(#"finish loading");
// put the scrolling table call here: [self scrollTableView ];
}
return nil;
}
Thank you all for helping!

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.

ipad two UITableView with data source linking to each other

Need to display data from a single datasource into two different tables arranged just like columns. Here if first uitableview loads upto row 10 the next uitableview should start from row 11 and also if first uitableview scrolls the next uitableview should autoscroll the data based on the first one.
Well for loading your tables you can use cellForRowAtIndexPath method and use tags to differentiate tables and for table1 use [array1 objectAtIndex:indexPath.row] and for table2 use [array1 objectAtIndex:indexPath.row+10]
Now, about scrolling tables together. I've tried several methods but I succeed with this one:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
NSArray *tableCells = [table1 visibleCells];
if ([tableCells count] > 0)
{
NSIndexPath *index = [table1 indexPathForCell:[tableCells objectAtIndex:0]];
[table2 scrollToRowAtIndexPath:index atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
}
But here what will happen is when you're done scrolling one table, your second table will start scrolling. If you need simultaneous scrolling, I tried - (void)scrollViewDidScroll:(UIScrollView *)scrollView method but it wasn't working smoothly as required. If this solution helps you, I'll be glad but if you find a better solution do let me know as well.

Resources