9

I have a ViewController with a CollectionView inside. When the view loads, the visible cells (9 cells) are shown correctly. When I scroll down, I want to load the visible items in the collectionview with loadImagesForOnscreenRows by calling the indexPathsForVisibleItems for partnerCollectionView. But when loadImagesForOnscreenRows the indexPathsForVisibleItems has allways the first 9 cells in it, even when cells 10 to 18 should be visible on the screen. The code I use is:

#import "PartnerListViewController.h"
#import "AppDelegate.h"
#import "Partner.h"
#import "ImageLoader.h"
#import "PartnerDetailViewController.h"

@interface PartnerListViewController ()

@end

@implementation PartnerListViewController

@synthesize lblTitle;
@synthesize partnerCollectionView;

@synthesize imageDownloadsInProgress;

@synthesize fetchedResultsController;
@synthesize managedObjectContext;

- (void)viewDidLoad
{
   [super viewDidLoad];
   // Do any additional setup after loading the view.

   AppDelegate * appDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
   managedObjectContext = [appDelegate managedObjectContext];
   imageDownloadsInProgress = [NSMutableDictionary dictionary];
   appDelegate = nil;

   [self setupFetchedResultsController];
   [partnerCollectionView reloadData];
}

- (void)didReceiveMemoryWarning
{
   [super didReceiveMemoryWarning];
   // Dispose of any resources that can be recreated.
}

#pragma mark - Collection view data source

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return [[fetchedResultsController sections] count];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
   id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
   return [sectionInfo numberOfObjects];
}

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
   static NSString *CellIdentifier = @"PartnerCell";
   UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];

   Partner *partner = [self.fetchedResultsController objectAtIndexPath:indexPath];

   UIImageView *imageView = (UIImageView *)[cell viewWithTag:100];
   if (!partner.image)
   {
       imageView.image = [UIImage imageNamed:@"annotationMap.png"];
       if (self.partnerCollectionView.dragging == NO && self.partnerCollectionView.decelerating == NO)
       {
           [self startDownload:partner.imageUrl forIndexPath:indexPath];
       }
   } else {
       imageView.image = [UIImage imageWithData:partner.image];
   }

   UILabel *lblTitlePartner = (UILabel *)[cell viewWithTag:101];
   lblTitlePartner.text = partner.title;
   lblTitlePartner.font = [UIFont fontWithName:fontName size:10];

   return cell;
}

#pragma mark - Table cell image support
- (void)startDownload:(NSString *)urlString forIndexPath:(NSIndexPath *)indexPath
{
   NSLog(@"startDownload:%ld", (long)indexPath.row);

   ImageLoader *imageLoader = [imageDownloadsInProgress objectForKey:indexPath];
   imageLoader = [[ImageLoader alloc] init];
   imageLoader.urlString = urlString;
   imageLoader.indexPathTableView = indexPath;
   imageLoader.delegate = (id)self;
   [imageDownloadsInProgress setObject:imageLoader forKey:indexPath];
   [imageLoader startDownload];
}

// this method is used in case the user scrolled into a set of cells that don't have their app icons yet
- (void)loadImagesForOnscreenRows
{
   NSArray *visiblePaths = [self.partnerCollectionView indexPathsForVisibleItems];
   NSMutableArray *rowsArray = [NSMutableArray arrayWithCapacity:[visiblePaths count]];
   [visiblePaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
       NSLog(@"loadImagesForOnscreenRows1%@", @(indexPath.item));
       [rowsArray addObject:@(indexPath.item)];
   }];
   for (NSIndexPath *indexPath in visiblePaths)
   {
       NSLog(@"loadImagesForOnscreenRows2:%ld", (long)indexPath.row);

       Partner *item = [self.fetchedResultsController objectAtIndexPath:indexPath];

       if (!item.image) // avoid the app icon download if the app already has an icon
       {
           [self startDownload:item.imageUrl forIndexPath:indexPath];
       }
   }
}

// called by our ImageDownloader when an icon is ready to be displayed
- (void)imageLoaderDidFinishDownloading:(NSIndexPath *)indexPath
{
   NSLog(@"imageLoaderDidFinishDownloading:%ld", (long)indexPath.row);

   ImageLoader *imageLoader = [imageDownloadsInProgress objectForKey:indexPath];
   if (imageLoader != nil)
   {
       // Save the newly loaded image
       Partner *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
       item.image = UIImageJPEGRepresentation(imageLoader.image, 1.0);

       [self performSelectorOnMainThread:@selector(saveItem) withObject:nil waitUntilDone:YES];
       [self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:YES];
   }
}

- (void)saveItem
{
   [self.managedObjectContext save:nil];
}

- (void)reloadData
{
   [self.partnerCollectionView reloadData];
}

#pragma mark deferred image loading (UIScrollViewDelegate)

// Load images for all onscreen rows when scrolling is finished
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
   if (!decelerate)
{
       [self loadImagesForOnscreenRows];
   }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
   [self loadImagesForOnscreenRows];
}

- (void)setupFetchedResultsController
{
   // 1 - Decide what Entity you want
   NSString *entityName = @"Partner"; // Put your entity name here
   NSLog(@"Setting up a Fetched Results Controller for the Entity named %@", entityName);

   // 2 - Request that Entity
   NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];

   // 4 - Sort it if you want
   request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"title" ascending:NO selector:@selector(localizedCaseInsensitiveCompare:)]];
   // 5 - Fetch it
   self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
   NSError *error = nil;
  if (![[self fetchedResultsController] performFetch:&error]) {
      NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
   }
}

@end

And this result in the output:

Initial show of visible items

2013-09-02 06:45:21.940 [2564:c07] startDownload:0
2013-09-02 06:45:21.943 [2564:c07] imageLoaderDidFinishDownloading:0
2013-09-02 06:45:21.950 [2564:c07] startDownload:1
2013-09-02 06:45:21.951 [2564:c07] imageLoaderDidFinishDownloading:1
2013-09-02 06:45:21.958 [2564:c07] startDownload:2
2013-09-02 06:45:21.959 [2564:c07] imageLoaderDidFinishDownloading:2
2013-09-02 06:45:21.965 [2564:c07] startDownload:3
2013-09-02 06:45:22.063 [2564:c07] imageLoaderDidFinishDownloading:3
2013-09-02 06:45:22.072 [2564:c07] startDownload:4
2013-09-02 06:45:22.073 [2564:c07] imageLoaderDidFinishDownloading:4
2013-09-02 06:45:22.081 [2564:c07] startDownload:5
2013-09-02 06:45:22.082 [2564:c07] imageLoaderDidFinishDownloading:5
2013-09-02 06:45:22.089 [2564:c07] startDownload:6
2013-09-02 06:45:22.090 [2564:c07] imageLoaderDidFinishDownloading:6
2013-09-02 06:45:22.098 [2564:c07] startDownload:7
2013-09-02 06:45:22.099 [2564:c07] imageLoaderDidFinishDownloading:7
2013-09-02 06:45:22.104 [2564:c07] startDownload:8
2013-09-02 06:45:22.163 [2564:c07] imageLoaderDidFinishDownloading:8

After scrolling to item 10 to 19:

2013-09-02 06:45:26.212 [2564:c07] loadImagesForOnscreenRows1:8
2013-09-02 06:45:26.212 [2564:c07] loadImagesForOnscreenRows1:0
2013-09-02 06:45:26.212 [2564:c07] loadImagesForOnscreenRows1:1
2013-09-02 06:45:26.212 [2564:c07] loadImagesForOnscreenRows1:6
2013-09-02 06:45:26.213 [2564:c07] loadImagesForOnscreenRows1:2
2013-09-02 06:45:26.213 [2564:c07] loadImagesForOnscreenRows1:3
2013-09-02 06:45:26.213 [2564:c07] loadImagesForOnscreenRows1:4
2013-09-02 06:45:26.213 [2564:c07] loadImagesForOnscreenRows1:5
2013-09-02 06:45:26.213 [2564:c07] loadImagesForOnscreenRows1:7
2013-09-02 06:45:26.214 [2564:c07] loadImagesForOnscreenRows2:8
2013-09-02 06:45:26.214 [2564:c07] loadImagesForOnscreenRows2:0
2013-09-02 06:45:26.214 [2564:c07] loadImagesForOnscreenRows2:1
2013-09-02 06:45:26.214 [2564:c07] loadImagesForOnscreenRows2:6
2013-09-02 06:45:26.214 [2564:c07] loadImagesForOnscreenRows2:2
2013-09-02 06:45:26.215 [2564:c07] loadImagesForOnscreenRows2:3
2013-09-02 06:45:26.215 [2564:c07] loadImagesForOnscreenRows2:4
2013-09-02 06:45:26.215 [2564:c07] loadImagesForOnscreenRows2:5
2013-09-02 06:45:26.215 [2564:c07] loadImagesForOnscreenRows2:7

As you can see after scrolling, the items visible index paths do stay the same. Has anybody else encountered this or an idea for a solution? Or am I unsterstanding some principle of collectionview wrong?

Many thanks in advance! Kind regards, Jan

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Jan Stulens
  • 141
  • 1
  • 1
  • 3
  • I don't get the purpose of `loadImagesForOnscreenRows`. In `cellForRowAtIndexPath` you set the image if it's available. Otherwise, in `imageLoaderDidFinishDownloading` you can reload the index path, which will call `cellForRowAtIndexPath` again or you can set the image on the cell directly if it's still on screen. Wouldn't that cover it? – Timothy Moose Sep 03 '13 at 15:23
  • Hi Timothy, `cellForRowAtIndexPath` loads the cells that are visible in the view, in my case the first nine. When i start scrolling, the upcoming cells wont be filled in, instead `scrollViewDidEndDragging` is called, which will call `loadImagesForOnscreenRows`, like in tableviews. In this function I want to get the indexpaths, `indexPathsForVisibleItems`, that are visible, and start downloading the images, once these are downloaded, reload the indexpaths. But `indexPathsForVisibleItems` always returns the first nine. Or am I doing something completely wrong. With tables it works fine this way. – Jan Stulens Sep 03 '13 at 21:21
  • The cells that don't have images would typically get filled in individually when the download for that cell completes, which in your case would happen in `imageLoaderDidFinishDownloading`. Given this, I don't get the point of `loadimageforOnscreenRows`. In other words, why aren't you filling in the cell in `imageLoaderDidFinishDownloading`? – Timothy Moose Sep 03 '13 at 21:26
  • When I Load my view, `cellForRowAtIndexPath` is only called for the first nine cells, the visible ones. A `NSLog`in `cellForRowAtIndexPath` only prints 0 to 8. So the other cells won't start downloading, reloading the collectionView after fetching the result only impacts the visible cells. That's why I taught I should call `loadimageforOnscreenRows` on `scrollViewDidEndDragging`, so it can load the images for cells that are becoming visible after scrolling down. Because if i put the `scrollViewDidEndDragging` in comment, it does completely nothing after scrolling. :) – Jan Stulens Sep 03 '13 at 22:21
  • `cellForRowAtIndexPath` is always called before a cell is displayed on screen. Otherwise, there wouldn't be anything to display. Also, you don't seem to be implementing the `NSFetchedResultsControllerDelegate` methods, so I'm unclear why you're even using an `NSFetchedResultsController` because, without a delegate, it isn't controlling anything. Am I missing something? – Timothy Moose Sep 03 '13 at 23:32
  • Hi Timothy, I have another problem. Using `reloadData` on the collectionview in `imageLoaderDidFinishDownloading`, doesn't call `cellForRowAtIndexPath` for the cells becoming visible. When I use `reloadItemsAtIndexPaths`, `cellForRowAtIndexPath` is called for the cells becoming visible. The only problem is that the first cell of every row keeps its default image and the others are refreshed properly. When I scroll the first cells out of the screen and back in, they appear properly. So the download is correct, only refreshing doesn't seem to work. Do you have any idea? Many thanks in advance! – Jan Stulens Sep 16 '13 at 18:51
  • **indexPathsForVisibleItems** seems to return an Array of Visible Items in vertical order. For instance, if you have 3 columns in your UICollectionView, **indexPathsForVisibleItems** will return the visible items available in the first column (0, 3, 6). Then, the second column (1, 4, 7). Lastly, the third column (2, 5, 8). That means your order returned will be 0, 3, 6, 1, 4, 7, 2, 5, 8 which works slightly different than a tableview that would return 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. If you really need to load the images in horizontal order, you can sort the array returned before proceeding. – bvmobileapps Oct 02 '15 at 02:16

1 Answers1

6

If you are targeting iOS 8.0 and above you should use collectionView:willDisplayCell:forItemAtIndexPath: to kick off your download. If using iOS 7.0 you should continue to use collectionView:cellForItemAtIndexPath:.

In your imageLoaderDidFinishDownloading: callback you should check to see if the index path in question is still visible. If it is, retrieve the corresponding cell and update its image view. If the cell isn't visible, then your work is done. Calling -reloadData for every image completion is doing a lot of expensive work and could have significant UX issues if your user is currently mid-scroll of the table and you reset its contents. You are also potentially doing the UIImageJPEGRepresentation() work many times, it would help your scrolling performance if you did this work once in imageLoaderDidFinishDownloading: and then cached it.

Since it looks like the callback happens on a background thread make sure you only manipulate the UICollectionView from the main thread.

xaphod
  • 6,392
  • 2
  • 37
  • 45
jszumski
  • 7,430
  • 11
  • 40
  • 53