iOS: SDWebImage loads cell with incorrect images - ios

I'm currently using SDWebImage to load pictures for my table cells, using the following code:
[cell.coverImage sd_setImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] placeholderImage:[UIImage imageNamed:#"imageplaceholder_general"]];
The problem is when I scroll up and down, the images were inserted into the wrong cells. After reading some post on StackOverflow regarding this issue, I suspect it to be due to that cells are reused when we scroll and hence the asynchonous download of the image may be placed on a cell indexPath that has changed.
Hence I implemented several changes e.g.:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
UIImageView * cellCoverImage = cell.coverImage;
[manager downloadImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL * oriURL) {
NSArray *visibleIndexPaths = [tableView indexPathsForVisibleRows];
if ([visibleIndexPaths containsObject:indexPath]) {
cellCoverImage.image = image;
}
}];
Or even to compare URLs:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
UIImageView * cellCoverImage = cell.coverImage;
[manager downloadImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL * oriURL) {
if([oriURL isEqual:[self.dataInJSONModel.Content[indexPath.row] CoverImage]])
{
cell.coverImage.image = image;
}
}];
Still the problem persist. Or I might have wrongly programmed it? Found several suggestions online but no concrete solution yet seen.
Need help!
EDIT
I've already made some changes to it but still doesn't work:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NewsFeedCell * cell = [tableView dequeueReusableCellWithIdentifier:#"NewsFeedCell" forIndexPath:indexPath];
if (self.dataInJSONModel)
{
cell.coverImage.image = nil;
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL * oriURL) {
if ([cell isEqual:[self.tableView cellForRowAtIndexPath:indexPath]])
{
cell.coverImage.image = image;
}
}];
}

I met the same problem, and tried assign .image = nil, but not work.
Finally, my sloution is to override prepareForReuse in UITableViewCell with cancel operation:
- (void)prepareForReuse
{
[super prepareForReuse];
[_imageView sd_cancelCurrentImageLoad];
}

Posted the question on the SDWebImage Github page and gotten a suggestion from someone who solves my problem! I just override the prepareForReuse method in my cell's implementation file and nullify the image of the affected imageView.
Sample code for future reader:
In my NewsFeedCell.m
- (void) prepareForReuse
{
[super prepareForReuse];
self.coverImage.image = NULL;
}
And this solves the problem! My opened issue at GitHub is https://github.com/rs/SDWebImage/issues/1024, should any of you want to see.

I tried most of these solutions, spent some time fixing this. I got 2 solutions working for me.
When setting image to ImageView in cells stop download.
In cellForRow add this:
cell.imageView.sd_cancelCurrentImageLoad()
and then in cell:
func prepareForReuse() {
imageView.image = UIImage.placeholderImage() // or nill
}
This is not real solutions because your actually stop image download and waste already downloaded data.
Another more elegant solutions is adding extension for UIImageView:
Choose animation which suits you, and try this:
func setImageAnimated(imageUrl:URL, placeholderImage:UIImage) {
self.sd_setImage(with: imageUrl, placeholderImage: placeholderImage , options:SDWebImageOptions.avoidAutoSetImage, completed: { (image, error, cacheType, url) in
if cacheType == SDImageCacheType.none {
UIView.transition(with: self.superview!, duration: 0.2, options: [.transitionCrossDissolve ,.allowUserInteraction, .curveEaseIn], animations: {
self.image = image
}, completion: { (completed) in
})
} else {
self.image = image
}
})
}

You are right in your analysis of the problem, just not executed it quite correctly.
some pseudocode for cellForRowAtIndexPath...
- set the cell.imageView.image to nil to wipe out
previous use of cell image view contents
- get the URL from data source
- initiate asynchronous download with completion ^{
//check the cell is still the correct cell
if ([cell isEqual: [collectionView cellForRowAtIndexPath:indexPath]]) {
cell.imageView.image = image
}
}
A couple of things you are doing wrong
- don't grab a reference to the cell's image view until you know you need it (in the completion block)
- don't check visibleIndexPaths, the indexPath might still be visible but allocated to a different cell (if you have scrolled off then on again for example). The 'cell isEqual' technique I use here should suffice for all cases.
You can also nil out old cell contents for a recycled cell by overriding the cells -prepareForReuse method.

The Obvious error here is that you're not accounting for using Blocks. Essentially, the completion happens on a background thread, and all UI updates must happen on the Main Thread.
The simple solution is;
dispatch_async(dispatch_get_main_queue(), ^{
cell.coverImage.image = image;
});
Further, if you intend to reference the tableView in your completion block, you should use a weak reference to it.

Related

Swift - not working correctly IOS 9.2

I have added a table view, and I am display image in the cells. I have also added this code:
So that the cells resize depending on the image.
When I launch my app though, I get this : [![enter image description here][1]][1]
And the images do not load untill I start scrolling...If I scroll down half the page then go back to the top, I get this: Which is correct
[![enter image description here][2]][2]
Any ideas? I have researched on google and tried the odd solution for the older versions of Xcode, But nothing seems to work!
Here is the rest of my code from the TableViewController:
Image isn't loaded correctly in cellForRowAtIndexPath delegate method, you're (probably) downloading the image in the background, so cellForRowAtIndexPath is returned before image is ready.
Downloaded image is probably cached somewhere so next time it's loaded properly.
post.downloadImage() better have a callback closure to be called when image was downloaded, to assign the downloaded image into the proper cell.
Keep in mind that user may scroll this cell out of the screen before image is loaded, so you better use a unique id to abort downloaded image assignment if cell has already changed.
Here's an example for a method that downloads an image in the background, then assigns it to the cell -
+ (void)loadImage:(NSString *)imageUrl onComplete:(void(^)(UIImage *image, BOOL loaded, NSString *callIdentifier))callback callIdentifier:(NSString *)callIdentifier {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul), ^{
[self downloadPicture:url onComplete:^(UIImage *image, BOOL loaded) {
dispatch_sync(dispatch_get_main_queue(), ^{
callback(image, loaded, callIdentifier);
});
}];
});
callback([UIImage imageNamed:#"placeholder"], NO, callIdentifier);
}
+ (void)downloadPicture:(NSString *)url saveTo:(NSString *)filePath onComplete:(void (^)(UIImage *image, BOOL loaded))onComplete {
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url] options:NSDataReadingMappedAlways error:&error];
if (!error) {
UIImage *image = [UIImage imageWithData:data scale:GTUserPictureScale];
if (onComplete)
onComplete(image, YES);
} else {
NSLog(#"Error loading user picture: %#", [error description]);
if (onComplete)
onComplete([UIImage imageNamed:#"missing"], NO);
}
}
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// ...
__weak MyClass *wself = self;
self.imageUrl = #"http://...";
[self loadImage:self.imageUrl onComplete:^(UIImage *image, BOOL loaded, NSString *callIdentifier) {
#synchronized(wself) {
if ([callIdentifier isEqualToString:wself.imageUrl]) {
if (loaded) {
wself.image = image;
}
} else
NSLog(#"Expired load image request for id %#", callIdentifier);
}
} callIdentifier:self.imageUrl];
// ...
}

Download Image Edit image then Cache

UIImageview + afnetworking downloads images and caches the images.
But in certain cases the server images are = 15mb. So i need to compress them based on the some factor and make it to 1mb and then require to cache them.
SDWebImageCache on the other hand make you to define your own cache and store them
Is there any build in mechanism for downloading,editing and then later saving into the cache?
[SDWebImageDownloader.sharedDownloader downloadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize)
{
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished)
{
if (image && finished)
{
// do something with image
}
}];
then use
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey]
Is there any other alternative to doing something like this?
Your scenario with SDWebImage is correct.
For editing purpose you need set delegate to SDWebImageManager object and implement necessary method:
// Set delegate
[SDWebImageManager sharedManager].delegate = self;
// Implement delegate method
- (UIImage *)imageManager:(SDWebImageManager *)imageManager
transformDownloadedImage:(UIImage *)image
withURL:(NSURL *)imageURL {
UIImage scaledImage = ... // Make scale based on 'image' object
return scaledImage;
}
Note that this method called immediately after image was downloaded but before storing it to memory cache and before completion block is called.
Documentation for this method:
Allows to transform the image immediately after it has been downloaded
and just before to cache it on disk and memory. NOTE: This method is
called from a global queue in order to not to block the main thread.
After that you will be able to use SDWebImageDownloader and SDImageCache as in your question:
[SDWebImageDownloader.sharedDownloader downloadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (image && finished) {
[[SDImageCache sharedImageCache] storeImage:image forKey:myCacheKey];
}
}];
Then you can manage cache by using methods of SDImageCache class:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
If you need algorithm for image scaling by max data size take a look on this answer.

Parse PFFile download order iOS

I'm storing 5 PFFiles in an array and using getDataInBackgroundWithBlock to download those files from Parse.
The problem is the order at which they appear in the table view cells is different every time, presumably because the files are download at different speeds due to the different file sizes.
for (PFFile *imageFile in self.imageFiles) {
[imageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *avatar = [UIImage imageWithData:imageData];
[self.avatars addObject:avatar];
cell.userImageView.image = self.avatars[indexPath.row];
}
}];
}
The self.imageFiles array is in the correct order.
How do I ensure that the images downloaded are added to the self.avatars array in the same order as the self.imageFiles?
The question has two parts: (1) explicitly, how to maintain the order of results of asynchronous operations, (2) implied by the use of cell, how to properly handle asynch requests in support of a tableview.
The answer to the first question is simpler: keep the result of the request associated with the parameter for the request.
// change avatars to hold dictionaries associating PFFiles with images
#property(nonatomic,strong) NSMutableArray *avatars;
// initialize it like this
for (PFFile *imageFile in self.imageFiles) {
[avatars addObject:[#{#"pfFile":imageFile} mutableCopy]];
}
// now lets factor an avatar fetch into its own method
- (void)avatarForIndexPath:(NSIndexPath *)indexPath completion:^(UIImage *, NSError *)completion {
// if we fetched already, just return it via the completion block
UIImage *existingImage = self.avatars[indexPath.row][#"image"];
if (existingImage) return completion(existingImage, nil);
PFFile *pfFile = self.avatars[indexPath.row][#"pfFile"];
[pfFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *avatar = [UIImage imageWithData:imageData];
self.avatars[indexPath.row][#"image"] = avatar;
completion(avatar, nil);
} else {
completion(nil, error);
}
}];
}
Okay for part (1). For part 2, your cellForRowAtIndexPath code must recognize that cells are reused. By the time the asynch image fetch happens, the cell you're working on might have scrolled away. Fix this by not referring to the cell in the completion block (only the indexPath).
// somewhere in cellForRowAtIndexPath
// we're ready to setup the cell's image view
UIImage *existingImage = self.avatars[indexPath.row][#"image"];
if (existingImage) {
cell.userImageView.image = existingImage;
} else {
cell.userImageView.image = // you can put a placeholder image here while we do the fetch
[self avatarForIndexPath:indexPath completion:^(UIImage *image, NSError *error) {
// here's the trick that is often missed, don't refer to the cell, instead:
if (!error) {
[tableView reloadRowsAtIndexPaths:#[indexPath]];
}
}];
}
Reloading the row in the completion block will cause cellForRowAtIndexPath to be called again, except on that subsequent call, we'll have an existing image and the cell will get configured immediately.
Whilst danh's answer has answered my question, I did manage to solve it shortly after posting the question. I'm capturing the index of each imageFile and making sure they are added to the self.avatars array in that order.
for (PFFile *imageFile in self.imageFiles) {
NSInteger index = [self.imageFiles indexOfObject:imageFile];
[imageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *avatar = [UIImage imageWithData:imageData];
self.avatars[index] = avatar;
[self.tableView reloadData];
}
}];
}
Then cell.userImageView.image = self.avatars[indexPath.row]; in cellForRowAtIndexPath:

How to get time interval beetween uiimage.image property change and actually image appears from web

I am using SDWebimage-master image downloader
it takes time to appear an image about to 8 sec.
i want to get notification of image at appear time (not .image property change time)
code:
thumbsize:
[imgFull setImageWithURL:thumbURL placeholderImage:nil];
fullsize :
[imgFull setImageWithURL:fullsizeURL placeholderImage:nil];
You can use this method . it will give you progress in progress block and completed block will call while you getting image. but you should check if(finished) because after finish you can get whole image.
[[SDWebImageManager sharedManager]downloadWithURL:[NSURL URLWithString:url]
options:SDWebImageProgressiveDownload
progress:^(NSInteger receivedSize, NSInteger expectedSize
{
//you can show progress here if you want/
//float progress = receivedSize / (float)expectedSize;
//[progressview setProgress:MAX(MIN(1, progress), 0) animated:YES];
}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished)
{
if (finished)
{
//download Finish.
}
}];
Hope this will help you.

Custom UITableViewCell - Asynchronous UIImage for UIImageView

I have created a custom UITableViewCell which is composed by 2 UILabels and a single UIImageView.
Data associated with cells is available with a NSObject class named CellInfo. CellInfo has 2 properties of NSString type and an UIImage property.
When I create a CellInfo instance, inside the initWithData method (CellInfo class), I do the following:
if(self = [super alloc])
{
//initialize strings variables
self.name = aName;
self.descritpion = aDescription;
[self grabImage]
}
return self;
where grabImage (within CellInfo class) using ASIHTTPrequest framework to grab images in asynchronous manner (in the following code NSURL is alaways the same but in reality it changes with data)
- (void)grabImage
{
NSURL *url = [NSURL URLWithString:#"http://myurl.com/img.png"];
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
NSData *data = [request responseData];
UIImage* img = [[UIImage alloc] initWithData:data];
self.image = img;
[img release];
// Send a notification if image has been downloaded
[[NSNotificationCenter defaultCenter] postNotificationName:#"imageupdated" object:self];
}];
[request setFailedBlock:^{
NSError *error = [request error];
// Set default image to self.image property of CellInfo class
}];
[request startAsynchronous];
}
I have also a UITableViewController that loads data into the custom cell like the following:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Do stuff here...
// Configure the cell...
((CustomTableViewCell*)cell).nameOutlet.text = ((CellInfo*) [self.infoArray objectAtIndex:indexPath.row]).name;
((CustomTableViewCell*)cell).descriptionOutlet.text = ((CellInfo*) [self.infoArray objectAtIndex:indexPath.row]).descritpion;
((CustomTableViewCell*)cell).imageViewOutlet.image = ((CellInfo*) [self.infoArray objectAtIndex:indexPath.row]).image;
return cell;
}
In addiction, this UITableViewController observes notification from the CellInfo class because, at start up, images for visible cells are not displayed. This is the method that is called when the notification is captured:
- (void)imageUpdated:(NSNotification *)notif {
CellInfo * cInfo = [notif object];
int row = [self.infoArray indexOfObject:cInfo];
NSIndexPath * indexPath = [NSIndexPath indexPathForRow:row inSection:0];
NSLog(#"Image for row %d updated!", row);
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
The code works well, but I would like to know if I'm doing right or there is a better way to do this.
My doubt is the following: is it correct to save downloaded images within each CellInfo instance or is it possible to follow another way to cache images using, for example, cache policy provided by ASIHTTPRequest?
P.S. grabImage is not called if the image for a specific CellInfo instance has already been downloaded.
I believe that's pretty neat. Instead of that you might subclass UIImageView class and create an initializer like [AsyncUIImageView initWithURL:] and then put that ASIHttpRequest logic inside the view.
After it finishes loading the picture, there could be two ways:
It can call [self setNeedsDisplay] (an UIView method) so image view is redrawn.
You can pass UITableViewCell or UITableView as a delegate to AsyncUIImgeView so that it could tell table view to reload that cell.

Resources