UITableView with infite scrolling and lazy loading - iphone

I got an UITableView which is filled with an unknown amount of rows. Each row contains (for example 3) images and the application gathers this information from a webservice.
For the UITableView I implemented infinite scrolling. Whenever the table almost reaches the end of the current amount of rows, I initiate a call to the webservice, to fetch the next 50 rows of data. I do this in the scrollViewDidScroll method which is called by the UITableView.
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat actualPosition = scrollView.contentOffset.y;
float bottomOffset = 450;
if([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)
bottomOffset = 1000;
CGFloat contentHeight = scrollView.contentSize.height - bottomOffset;
if (actualPosition >= contentHeight && !_loading && !_cantLoadMore)
{
self.loading = YES;
_currentPage++;
NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
[dict setObject:[NSNumber numberWithInt:_currentPage] forKey:#"page"];
[dict setObject:[_category objectForKey:#"ID"] forKey:#"category"];
[self performSelectorInBackground:#selector(getWallpapersInBackground:) withObject:dict];
}
}
The images in the rows of the UITableView are lazy loaded. Whenever a row is visible, the images are loaded in a separate thread and the rows are updated once an image is fully downloaded. The principle I use for lazy loading, is the same as Apple suggests in their documentation. The image is also added to a local NSDictionary, so I can fetch it when the row scrolls out of the screen and back in (and the row is recreated).
Because the amount of pictures in a single view can go up to 2000 - 3000, I also cache the images to disk and clear out the images from the NSDictionary when they are further than X rows away. When the user scrolls down and up again the following happens:
New rows are displayed
Lazy loading method is called, which checks if the image is present on disk or that it should download it.
When the image is downloaded or fetched from disk, it performs a codeblock, which displays the image in the row. Images downloaded from the internet, are also cached to disk.
UIImage is added to NSDictionary for faster caching of images that need to be within reach.
Images which are in rows, 15 rows or further from the visible rows are removed from the NSDictionary, because of memory problems (too many UIImage objects in the NSDictionary cause out-of-memory errors).
When the UITableView almost reaches the end, the following occurs:
UITableView almost reaches end of currently loaded rows
Call to webservice, with loads of new rows (50)
The new rows are added to an array, which is used for the numberOfItemsInSection method.
A reloadData is called to make sure the UITableView populates with the extra new rows.
New rows that contain images, performs the steps mentioned above to lazy load the images.
So, the problem I have is when I am adding new records from the webservice. When I call a reloadData on the UITableView some images are loading from disk again and some hickups occur while scrolling.
I am looking for a solution for this. I tried using insertItemsAtIndexPaths with the amount of new rows, to add them to the UITableView, but this makes my lazy load method already download the images (because somehow all the cells are created at that time, even when they are not visible, checking if cell is visible during creation delivers unexpected results, images not loading, cells look weird, etc).
So, what I essentially am looking for is a solution for an infinite scrolling UITableView, which lazy loads images from the web/disk and is as smooth as the photo application. Since images are all loaded in separate threads, I don't understand why scrolling isn't as smooth as a baby's skin.

I'm not sure if I totally grok the fullness of your question, but here are some things that I did when confronted with a similar problem.
I used -tableView:cellForRowAtIndexPath: as my cue to load more data from the web. I picked a number (in my case 10), when indexPath.row + 10 >= self.data.count load more data.
I needed to call -tableView:cellForRowAtIndexPath: anyway.
I didn't force a callback into the tighter loop of -scrollViewDidScroll:.
I subclassed UIImageView with a class that would async load images which I called URLImageView.
In -tableView:cellForRowAtIndexPath:, I assigned a URL to the URLImageView instance.
That assignment caused a background operation that would either load from disk or load from the web.
The operation was cancelable, so I didn't keep working on an image that no longer needed loading.
I had a table view with hundreds (but not thousands) of images. I only had 2 or 3 table cells on the screen at once.
I used -refreshData: it worked like a champ.
I even got URLImageView to fade in the image. It was a very nice effect.
I hope that helps.

to avoid the flickering try calling beginUpdateson the table, then insertrowAtIndexPaths:withRowAnimations: and finish with endUpdates.
Good luck.

Related

How to tell UICollectionView to preload a larger range of cells?

I have a UICollectionView which shows images retrieved from the web. They are downloaded asynchronous.
When user scrolls fast, they see placeholders until the cell loads. It seems UICollectionView only loads what is visible.
Is there a way to say "collection view, load 20 cells more above and below" so chance is higher that it loaded more cells while user was looking at content without scrolling?
The idea is to have the VC recognize when a remote load might be required and start it. The only tricky part is keeping the right state so you don't trigger too much.
Let's say your collection is vertical, the condition you want to know about is when:
BOOL topLoad = scrollView.contentOffset.y < M * scrollView.bounds.size.height
or when
BOOL bottomLoad = scrollView.contentOffset.y > scrollView.contentSize.height - M * scrollView.bounds.size.height
in other words, when we are M "pages" from the edge of the content. In practice though, this condition will be over-triggered, like when you're first loading, or if you're testing it on scrollViewDidScroll, you don't want to generate web requests for every pixel of user scrolling.
Getting it right, therefore, requires additional state in the view controller. The vc can have a pair of BOOLs, like topLoadEnabled, bottomLoadEnabled, that are NO until the view is ready. Then, scroll delegate code looks like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// compute topLoad and bottomLoad conditions
if (topLoad && self.topLoadEnabled) [self startTopLoad];
similarly for bottom. The load code looks like this, abstractly:
self.topLoadEnabled = NO; // don't trigger more loading until we're done
[self.model getMoreTopStuff:^(NSArray *newStuff, NSError *error) {
// do view inserts, e.g. tableView.beginUpdates
self.topLoadEnabled = YES;
}];
Same idea for bottom. We expect the model to fetch for itself (maybe the model has image urls) and cache the result (then the model has images). As the datasource for the view, the view controller gets called upon to configure view cells. I can just naively ask the model for images. The model should answer either fetched images or placeholders.
Hope that makes sense.
In my opinion you are making the wrong assumption: cells are just views so you shouldn't treat them as model objects. UICollectionView and UITableView are very efficient because they constantly recycle cells so you should think in therms of pre loading content in the business side of things. Create interactor or viewmodel objects and populate your data source with those, then you'll be able to ask those objects to preload images, if you still wish to do so.
A BOOL flag seldom is the answer. I'd rather go for estimating a reasonable page size and fetching images as needed from the cellForItemAtIndePath method.

Crash while reloading row and the whole table at the same time

I have a nice bug here, specially in slow devices.
I have a UITableView jusk like the ones in the App Store app, with a Load More button at the bottom.
When it is pressed, I load the new info from the server, set the array, and reload the table view. In that info is also the url of the image for each object displayed in the table rows, so when we got it we start donwloading the image. When the image is loaded, I reload the row of the object.
[self.searchResultsTableView reloadRowsAtIndexPaths: [NSArray arrayWithObject: path] withRowAnimation: UITableViewRowAnimationNone];
The problem comes when the row is being reloading because the image was downloaded, and the whole table is doing so cause the user pressed the load more button and more info was gotten. Then I get this error:
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (140) must be equal to the number of rows contained in that section before the update (125), plus or minus the number of rows inserted or deleted from that section (1 inserted, 1 deleted).
Which is perfectly understable, the table was enlarged while the new image row was being updated.
My question is: how can lock it so I do not reload that row until the whole table is reloaded? It would nice something like table.isReloading. I try with a boolean lock, and doing both actions in the main thread, but it did not work ...
Thanks a lot.
Well, a little research in the doc put some more light in all this.
reloadRowsAtIndexPaths:withRowAnimation: Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.
And that is what I did, I got the cell and set the new value for image, instead of reloading the whole of it. I guess it is more efficient also:
AHMyTableViewCell *cell = (AHMyTableViewCell *) [self.myTableView cellForRowAtIndexPath:path];
if ((cell != nil) && ([cell isKindOfClass:[AHMyTableViewCell class]]))
cell.myImageView.image = image;
It does not crash now.

how to implement cyclic scrolling on tableview

Could you please help me with circular scrolling in tableview please.
I want that if I scroll down tableview, the rows should go in the reverse way --
it should appear that move back around (bottom rows go around and now come back down from the top) i.e, cyclic scrolling basically.
How can I do so. Any suggestions please.
Thanx in advance.
You could "fake" the cyclic scrolling repeating the same cells all over again. In the numberOfRowsInSection method, return n times the actual number of rows. Make sure n is big enough.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return numberOfActualRows*100;
}
Then in the cellForRowAtIndexPath method (and elsewhere) use the mod operator (%) to return the proper cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger actualRow = indexPath.row % numberOfActualRows;
...
}
You may want to hide the sroll indicator.
self.tableView.showsVerticalScrollIndicator = NO;
You may also want to scoll the table view to the middle before you display the table so scrolling backwards works fine.
[self.tableView scrollToRowAtIndexPath: [NSIndexPath indexPathForRow:[self tableView:self.tableView numberOfRowsInSection:0]/2 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
Of course, the user would eventually hit the bottom or the top if he/she kept scrolling over and over.
This question has already been asked: implementing a cyclic UITableView
I'm copying that answer here to make it easier because the asker hasn't ticked my answer.
UITableView is same as UIScrollView in scrollViewDidScroll method.
So, its easy to emulate infinite scrolling.
double the array so that head and tail are joined together to emulate circular table
use my following code to make user switch between 1st part of doubled table and 2nd part of doubled table when they tend to reach the start or the end of the table.
:
/* To emulate infinite scrolling...
The table data was doubled to join the head and tail: (suppose table had 1,2,3,4)
1 2 3 4|1 2 3 4 (actual data doubled)
---------------
1 2 3 4 5 6 7 8 (visualising joined table in eight parts)
When the user scrolls backwards to 1/8th of the joined table, user is actually at the 1/4th of actual data, so we scroll instantly (we take user) to the 5/8th of the joined table where the cells are exactly the same.
Similarly, when user scrolls to 6/8th of the table, we will scroll back to 2/8th where the cells are same. (I'm using 6/8th when 7/8th sound more logical because 6/8th is good for small tables.)
In simple words, when user reaches 1/4th of the first half of table, we scroll to 1/4th of the second half, when he reaches 2/4th of the second half of table, we scroll to the 2/4 of first half. This is done simply by subtracting OR adding half the length of the new/joined table.
Written and posted by Anup Kattel. Feel free to use this code. Please keep these comments if you don't mind.
*/
-(void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat currentOffsetX = scrollView_.contentOffset.x;
CGFloat currentOffSetY = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height;
if (currentOffSetY < (contentHeight / 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY + (contentHeight/2)));
}
if (currentOffSetY > ((contentHeight * 6)/ 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY - (contentHeight/2)));
}
}
P.S. - I've used this code on one of my apps called NT Time Table (Lite). If you want the preview, you can check out the app: https://itunes.apple.com/au/app/nt-time-table-lite/id528213278?mt=8
If your table can sometimes be too short, at the beginning of the above method you can add a if logic to exit when data count is say for example less than 9.
I haven't done this myself, but you could try the approach you'd use with a UIScrollView to implement cycling scrolling of views (after all UITableView is a subclass of UIScrollView).
I would do as follows:
Create a UITableView with an arbitrary number of cells (at least 7 but will need more to prevent fast scrolling bumping at the end)
Position your UITableView so the centre cell is visible
Maintain a pointer to the index of the cell you are looking to display
In your cellForRowAtIndexPath: use your pointer to as an offset and add the row to it to get the cell that you want
When the UITableView has stopped moving (your UITableViewDelegate can serve as UIScrollViewDelegate so you can use scrollViewDidEndDecelerating). Set your offset index to the current cell, move the table view back to the centre cell without animation and reload the data.
The issue you will have is if the user keeps scrolling without stopping they will eventually hit the bumpers as the number of cells in the table is reached.
Hope this helps, and please post back if you get this working and it looks at all reasonable.
Regards
Dave
This is very much difficult to implement. However, take a look at the ScorllingMadness, which shows the demo of nested (cyclic) pages in a scroll-view.
You need to use the similar kind of trick here as UITableView is a subclass of UIScrollView.

Objective-C: Is there any event when a Table Cell goes away?

Is there any message I can override called when a Table Cell goes away (on scrolling the table)?
I think it should be something like dealoc.
I'm asking this because I have below situation:
I have a table with many cells (100+) and each of this cell contains a ImageView. For loading the image (from a URL) I'm using NSOperationQueue/NSInvocationOperation. The problem appears when user is scrolling the table before the image is completely loaded: because I'm reusing the cells the image is displayed in wrong cell.
To avoid this I'm thinking to use "cancelAllOperations" of NSOperationQueue object when the cell goes away.
Note: I've tried but is not working if I call this message on "prepareForReuse".
iOS 6:
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// Cancel operation here for cell at indexPath
}
Why not keeping the images in your table data source - the array that holds all the data for the table?
This way you won't have to load these images once again when scrolling back and it will solve your problem...
You can subclass UITableViewCell (or any UIView) and override willMoveToWindow:. It is called whenever the cell appears (or scrolls off screen).
When it goes out of the window the parameter will be nil:
- (void)willMoveToWindow:(UIWindow *)newWindow
{
[super willMoveToWindow:newWindow];
if (newWindow==nil) {
// Cell is no longer in window
}
}
If there were, it would be in the UITableViewDelegate class reference: http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UITableViewDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intf/UITableViewDelegate
The only thing they have is willDisplayCell which lets you do last minute adjustments BEFORE the cell appears. They don't have anything for when it disappears, but you could probably figure that out since there are only a certain number of cells on the screen at a time for a given cell height.
So if one is appearing and for a cell height of 80 for instance (in portrait mode so 480px screen height), then you can say that the one 6 cells away is about to disappear (6 cells * 80 pixels = 480). There are a couple other things to consider like which way you are scrolling, but you get the general idea.
Example Code: You should also look at lazy table loading via Apple's sample code http://developer.apple.com/iphone/library/samplecode/LazyTableImages/Introduction/Intro.html
I had the same issue and got some nice feedback in the developer forum. Quinn - The Eskimo from Apple:
As an aside, cancelling a network
small transfer because something has
scrolled off the screen is probably a
performance negative. For small
transfers, it's usually more efficient
to let it run to completion (and cache
the results in case they're needed in
the future). This is because of the
way that NSURLConnection manages HTTP
connection reuse. If you cancel a
transfer, NSURLConnection has to
either a) drop the underlying HTTP
connection on the floor, which means
it can't be reused, or b) continue
reading and just junk the data.
Neither of this is the best use of
resources.
Share and Enjoy
-- Quinn "The Eskimo!"
So, I'm not cancelling all the ImageDownload Operation, but rather only start them, when the user stops scrolling. Up to then only a placeholder is shown:
- (void)scrollViewWillBeginDragging:(UITableView *) tableView
{
self.dragging = TRUE;
}
- (void) scrollViewDidEndDragging: (UITableView *) tableView willDecelerate: (BOOL) decelerate
{
if(!decelerate && self.dragging)
[self loadThumbsForVisibleCells];
else
self.dragging = FALSE;
}
- (void) scrollViewDidEndDecelerating: (UITableView *) tableView
{
[self loadThumbsForVisibleCells];
}
Hope this helps!

Is there a better way to determine the right size for a UITableViewCell?

I have a UITableView cell that is going to have a variable size depending on it's content (potentially several lines of text).
SInce it appears that heightForRowAtIndexPath is called before I layout the cell, I just guess the correct height by calling [NSString sizeWithFont] on my text string. Is there a better way to set the height after I've laid out the text in the cell and have an idea of exactly what size it should be?
It's going to sound dumb, but ...uh... "layout your cell before you exit heightForRowAtIndexPath" ;)
Seriously, though -- the OS only ever calls this if it's going to be needed (as in: it's about to create the cell & display it on screen), so laying it out & getting ready to display is not wasted effort.
Note, you do not have to do your layout separately, logic-wise. Just make a call to your [self prepLayoutForCellAtIndex:index] within your heightForRowAtIndexPath routine.
If the data is static, you can create a height table and cache the info.
if (0 == heightTable[index]) {
heightTable[index] = [self prepLayoutForCellAtIndex:index];
}
return (heightTable[index]);
Heck, even if the data changes, you can either recalculate the table value in the method that changes the data, or clear to 0 so it gets recalculated the next time it's needed.
I use the following, usually:
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath: (NSIndexPath *) indexPath {
UITableViewCell *cell = [self tableView: tableView cellForRowAtIndexPath: indexPath];
return cell.bounds.size.height;
}
Note that I use this for tables where I pre-cache a bunch of rows ahead of time, not for those with a LOT of cells. However, for things like Settings tables, where there are just a few rows, but most likely very differently sized, it works well. For larger tables, I do something along the lines of what Olie suggested.
If you look at SMS.app as example, Apple saves the row height of the cell in the SMS.app sqlite database.