Incorrect image of CollectionView cell when loading and saving async file with Block completion

I understand that there are many of these issues, but none of them seem to address this case.

CollectionView reads the file names from the files array and loads images from the Documents directory or loads them, displays and saves them to Documents. It works perfectly smoothly and without any additional libraries, HOWEVER, when scrolling through several cells quickly, they get the wrong image. Calling [collectionView reloadData] with any action or scrolling reloads these incorrect images into good ones.

I assume this is due to the reuse of cells with an asynchronous image assignment, but how to solve this problem? CollectionView and custom cell defined in Storyboard. Images are stored on the server for basic authentication, so most reuse solutions are not applied.

 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { BrowseCollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"MyCell" forIndexPath:indexPath]; NSString *fileName = [files objectAtIndex:indexPath.item]; NSString *filePath = [thumbDir stringByAppendingPathComponent:fileName]; if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { cell.imageView.image = [UIImage imageWithContentsOfFile:filePath]; } else //DOWNLOAD { NSString *strURL = [NSString stringWithFormat:@"%@%@", THUMBURL, fileName]; NSURL *fileURL = [NSURL URLWithString:strURL]; cell.imageView.image = [UIImage imageNamed:@"placeholder.jpg"]; [self downloadFromURL:fileURL to:filePath completionBlock:^(BOOL succeeded, UIImage *image) { if (succeeded) { cell.imageView.image = image; } }]; } } cell.imageName = fileName; return cell; } - (void)downloadFromURL:(NSURL*)url to:(NSString *)filePath completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock { NSString *authStr = [NSString stringWithFormat:@"%@:%@", LOGIN, PASS]; NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding]; NSString *authValue = [authData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadRevalidatingCacheData timeoutInterval:30]; [request setValue:[NSString stringWithFormat:@"Basic %@", authValue] forHTTPHeaderField:@"Authorization"]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { if (!error) { UIImage *image = [[UIImage alloc] initWithData:data]; [data writeToFile:filePath atomically:NO]; completionBlock(YES, image); } else { completionBlock(NO, nil); } }]; } 

I tried a lot of changes, but none of them seemed to solve the problem. Currently, the effect is that new cells that appear during fast scrolling change more than once, and some of them always end up with the wrong image, indicating some reuse problem.

Thanks in advance for any help or suggestions.

+6
source share
3 answers

The problem is that with fast scrolling, a cell can be deleted and reused for another cell by the time the asynchronous request is completed, thereby updating the wrong cell. So, in your completion block, you need to make sure that the cell is still visible:

 [self downloadFromURL:fileURL to:filePath completionBlock:^(BOOL succeeded, UIImage *image) { if (succeeded) { BrowseCollectionViewCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath]; if (updateCell) { // if the cell is still visible ... updateCell.imageView.image = image; // ... then update its image } } }]; 

Note that this UICollectionView method UICollectionView cellForItemAtIndexPath not be confused with the similar UICollectionViewDataSource collectionView:cellForItemAtIndexPath: . The UICollectionView cellForItemAtIndexPath method returns a UICollectionViewCell if the cell is still visible, and returns nil if it is not.

By the way, the above assumes that the NSIndexPath for this cell cannot change (i.e. it is not possible for additional rows to be inserted above this row while the image was asynchronously extracted). This is sometimes incorrect. So, if you want to be careful, then you really need to go back to the model and recalculate what NSIndexPath for this model object, and use this when determining the appropriate updateCell help.

Ideally, as soon as you look at the above, there are several optimizations you can do for this process:

  • If the cell is being reused while the previous request is still running, you may need to cancel this previous request. If you do not, and you quickly scroll past, say, 200 cells, now showing, say, cells 201 to 221, requests for these 20 currently visible cells will be queued and not displayed until the previous 200 requests will not end.

    To undo previous requests, you cannot use sendAsynchronousRequest . You will have to use delegated NSURLConnection or NSURLSessionTask , which can be undone.

  • You should probably cache your images. If your currently visible cells all have their own images, and then you scroll them and turn them on again, you seem to be requesting them again. First you should see if you have already extracted the image, and if so, use it, and only if you do not have it in the cache, repeat the request.

    Yes, I know that you use the version of the file in the persistent storage for caching, but you can also cache in RAM. Usually NSCache used for this purpose.

This is a lot to change. If you want to help with this, let us know. But it’s much easier to use the UIImageView category in SDWebImage or AFNetworking , which does all this for you.

+14
source

You need to store contextual information somewhere. In other words, in your completion block, check if the cell should really contain the loaded image or not. If not, do not assign it. For example, the URL of the image. You can do it...

  • add the NSURL *imageURL property to the cell object
  • before your downloadFromURL:... save fileURL to the imageURL cell property
  • in the completion block, compare if fileURL matches cell.imageURL , and if it matches, set the image, otherwise don't set it
  • and nil property of your imageURL cell in prepareForReuse

... or you can move your upload code to your cell class and assign only the image URL, etc. Many ways to do this.

+13
source

You need to get a link to the cell in the replace block:

 [self downloadFromURL:fileURL to:filePath completionBlock:^(BOOL succeeded, UIImage *image) { if (succeeded) { cell.imageView.image = image; } }]; 

with

  [self downloadFromURL:fileURL to:filePath completionBlock:^(BOOL succeeded, UIImage *image) { BrowseCollectionViewCell *innerCell = (BrowseCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath]; if (succeeded) { innerCell.imageView.image = image; } }]; 
+2
source

All Articles