I realize there is a lot of these questions already, but none of them seem to address this case.
The CollectionView reads file names from files
array and either loads images from Documents directory or downloads them, displays and saves to Documents. Works perfectly smooth and without any additional libraries, HOWEVER when scrolling fast several cells receive wrong images. Calling [collectionView reloadData]
with any action or scrolling back reloads these wrong images to good ones.
I imagine that it's related to cell reusing with async image assigning, but how does one solve this issue? CollectionView and custom cell defined in Storyboard. Images are stored on server behind Basic Authentication, so most reuse solutions don't apply.
- (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've tried many modifications, but none of them seem to solve the problem. Currently, the effect is that new cells appearing during fast scroll are being changed more than once and some of them always end up with wrong image, pointing at some reuse issue.
Thanks in advance for any help and suggestions.
The issue is that when scrolling fast, the cell might be dequeued and reused for another cell by the time the asynchronous request finishes, thereby updating the wrong cell. So, in your completion block, you should make sure 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, this UICollectionView
method cellForItemAtIndexPath
should not be confused with the similarly named UICollectionViewDataSource
method collectionView:cellForItemAtIndexPath:
. The UICollectionView
method cellForItemAtIndexPath
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. that it's not possible that additional rows could have been inserted above this row while the image was being asynchronously retrieved). That sometimes is not a valid assumption. So, if you wanted to be careful, what you really should do is go back to the model and recalculate what the NSIndexPath
for this model object, and use that when determining the appropriate updateCell
reference.
Ideally, once you address the above, there are a few optimizations you could make to this process:
If the cell is reused while the previous request is still running, you might want to cancel that prior request. If you don't and you scroll quickly past, say, 200 cells, now showing, say, cells 201 through 221, the requests for those 20 currently visible cells will be queued up and won't display until the prior 200 requests finish.
To be able to cancel prior requests, though, you can't use sendAsynchronousRequest
. You'd have to use a delegate-based NSURLConnection
or NSURLSessionTask
, which are cancelable.
You probably should be caching your images. If your currently visible cells all have their images and you then scroll them off and back on, you appear to be re-requesting them. You should first see if you've already retrieved the image, and if so, use that, and only if you don't have one in your cache, re-issue the request.
Yes, I know you're using the version of the file in persistent storage to cache, but you might want to cache to RAM, too. Typically people use NSCache
for this purpose.
This is a lot to change. If you want help doing this, let us know. But far easier is to use the UIImageView
category in SDWebImage or AFNetworking, which does all of this for you.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With