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

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.

Related

Pushing a view controller after scrolling is complete

When a user adds an item to my list, I want to scroll to the new row, highlight it, and select it (which will push a new controller). The key part is waiting for the scroll animation to complete before pushing the new controller.
In this answer, I learned how to use the animation delegate to wait until the scroll is complete.
However, if the insertion row is already on scree, the table view will not scroll and the method will not fire.
How can I wait to push the new controller until the end of the scroll, and deal with the case where no scroll will be initiated - and how might I tell the difference between each case?
The easiest way to check whether a given row is visible in your table view is something like this:
if (!CGRectContainsRect([self.tableView bounds], [self.tableView rectForRowAtIndexPath:indexPath])
{
// the row is partially outside the table view’s boundaries and needs to be scrolled for full visibility
}
else
{
// the row is within the boundaries and does not need to be scrolled
}
Try creating a method to see if scrolling is needed. If no scrolling is needed, call the push right away, otherwise wait for the delegate call and push.
- (BOOL)isSrollingingNeededForIndexPath:(NSIndexPath *)indexPath {
NSArray *visibleIndices = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *visibleIndexPath in visibleIndices)
if ([indexPath compare:visibleIndexPath] == NSOrderedSame)
return NO;
return YES;
}
Edit: Good point. Since indexPathsForVisibleRows is used for data rendering.
You could do essentially the same thing with indexPathsForRowsInRect where you use the content.offset.y and the tableview.frame.size.height to determine your "visible rect".
Then to account for partially visible rows at the top and bottom you could add rowHeight-1 to the top of the rect and subtract rowHeight - 1 from the bottom of the rect. Code shouldn't be too gnarly if you have static height rows. If you have varying height rows it would still work, but it would be a bit more involved.
All said though, it seems like a lot of code for something which you'd think would have a simple answer.

UITableView with infite scrolling and lazy loading

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.

Scrolling two UITableViews together

I have a weird design case in which to implement I need to place two UITableViews on the same view. I would just use sections, but I need an index on the second table.
I'd like them to both scroll together like they were actually two sections of the same table view. Is this possible?
Here's a basic mockup illustrating why:
As far as I understand it, you cannot move the index. Also, when you add an index to a table, it appears over the entire tableview, not just one section.
If I have to implement this as two table views, they both need to scroll as if they were one. If it's possible to do this using sections, that's even better.
Update
After seeing your update my old answer is not very good but I'll leave my previous answer for anyone else who wants it.
New answer - It depends how you want the tables sync'd.
If you know
Which cells are in the top 5
How tall each cell is
The offset the table view
Then you can calculate which cell is visible at the top of the top 5 table. You can use this information to scroll the bottom table to the correct index.
Old answer
I'm with the other guys not sure why you would want to or if I am misinterpreting but it's pretty simple.
UITableView is a subclass of UIScrollView so you can set yourself as the UITableViewDelegate and override
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
{
UITableView *slaveTable = nil;
if (self.table1 == scrollView) {
slaveTable = self.table2;
} else if (self.table2 == scrollView) {
slaveTable = self.table1;
}
[slaveTable setContentOffset:scrollView.contentOffset];
}
This gives me something that looks like this:
The top UITableView is self.table1 and the bottom is self.table2 if either table is scrolled the scrolling is mirrored in the other table.
in swift this code:
func scrollViewDidScroll(scrollView: UIScrollView) {
if tb_time1 == scrollView {
tb_time2.contentOffset = tb_time1.contentOffset
}else if tb_time2 == scrollView {
tb_time1.contentOffset = tb_time2.contentOffset
}
}
From the mock-up it seems like this can be accomplished with a single UITableView with two sections.
Instead of maintaining two tableViews you could maintain two collections (one for each section). Then in cellForRowAtIndexPath choose the collection to use based on the section specified in the indexPath.

How do I obtain information about the pages in a UIScrollView?

I have a run of about 10 different methods, they all pass "page" and "index" between them in order to define a ScrollView with multiple pages. The majority of these methods run within a loop (for each page).
I'm working on a way to re-orient and re-position the pages depending on the device orientation. My trouble now is trying to get access to these multiple pages so I can run a method re-orienting them. Is there a way to loop through the pages and access their information (size, frame, contentSize etc.) so I can then just manipulate them?
You could put a pointer for each view into an array. Then iterating through the array config the Views like this:
-(void)configScrollViewForOriantation:(UIInterfaceOrientation)interfaceOrientation
//do an if on the oriantation and supply 2 versions...
//eg..
if(UIInterfaceOrientationIsPortrait(interfaceOrientation))
{
CGRect f;
for (UIViewController *page in pageArray) {
f = page.frame;
f.size.width = ..;
f.origin.x = ....;
page.contentSize = CGSizeMake(newWidth,newHeight)
//etc
}
else{
//landscape config
}
}
NB. If you called this method in a CAAffine animation it would animate the changes too.
Ideally you should only make one for loop and set some constants for the configs you are going to do based on the orientation.
Create a class that knows about the model data, and can divide it up into numbered pages. Now pass that object between your methods, instead of the raw page/index values. Now extend that object to work with your other orientation.

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!