UITableViewCell image async queue caching not working properly - ios

Can anyone tell me what is wrong with my code. It seems to work but when I scroll fast and then stop, the UIImageView cycles through a few pictures before displaying the correct one. This happens quickly but noticeable.
if (![post objectForKey:#"photoData"] && ![post objectForKey:#"isDownloadingPhoto"]){
cell.instagramPhoto.image = nil;
[[combinedArray objectAtIndex:indexPath.row] setObject:#"loading" forKey:#"photoData"];
[[combinedArray objectAtIndex:indexPath.row] setObject:#"yes" forKey:#"isDownloadingPhoto"];
[cell.instagramPhoto setAlpha:0];
if (instagramURL != nil){
dispatch_async(kBgQueue, ^{
int index = indexPath.row;
NSData* data = [NSData dataWithContentsOfURL: instagramURL];
if (data.length > 0){
[[combinedArray objectAtIndex:index] setObject:data forKey:#"photoData"];
[[combinedArray objectAtIndex:index] setObject:#"no" forKey:#"isDownloadingPhoto"];
UIImage *image = [UIImage imageWithData:data];
dispatch_sync(dispatch_get_main_queue(), ^{
cell.instagramPhoto.image = image;
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.4f];
cell.instagramPhoto.alpha = 1.0;
[UIView commitAnimations];
});
}
});
}
}
else if ([[post valueForKey:#"isDownloadingPhoto"] isEqualToString:#"yes"]) {
NSLog(#"loading");
cell.instagramPhoto.image = nil;
}
else {
NSData *data = [post valueForKey:#"photoData"];
NSLog(#"saved");
UIImage *image = [UIImage imageWithData:data];
cell.instagramPhoto.image = image;
}

Looks like this code is executed inside tableView:cellForRowAtIndexPath. And if you have any cell reusing logic here - you might have problems when cell will be reused before current async image download operation to this cell is complete.
You need to be able to abort these download tasks if cell is being reused before they complete.
** UPDATE **
There are couple ways you can implement it:
The easiest one (and probably most memory-consuming) - make sure you're generating unique cellId for each cell, so there will not be any reuse of already allocated cells. It will be slower, but you will have guarantee that operation you started will finish on the same cell you started it.
Check that cell is the same after your download is complete. You can for example at the beginning of your code assign tag for the cell to its row number.
cell.tag = indexPath.row
Then save it off before starting dataWithContext call and check after.
int oldTag = cell.tag;
dataWithContext...
if (cell.tag != oldTag) {
// Don't so any image update
}
Change dataWithContext to something async so you can actually break and abort the download process. I'm not sure how to proceed with this one - it will require a bit more of research.

Related

ios: overlap images in collection view cell

I have created collection view programmatically where i have created UIImageview programmatically within collectionViewCell and images are displayed in imageview are downloaded from server asynchronously.
Code below is written in cellForItemAtIndexPath: method -
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSURL *imageURL = [NSURL URLWithString:[arrmImgPaths objectAtIndex:indexPath.row]];
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
UIImage *image = [UIImage imageWithData:imageData];
// [imgvMainView removeFromSuperview];
// Now the image will have been loaded and decoded and is ready to rock for the main thread
dispatch_sync(dispatch_get_main_queue(), ^{
imgvMainView =[[UIImageView alloc] initWithFrame:CGRectMake(34,41,177,124)];
[cell addSubview:imgvMainView];
[imgvMainView setImage:image];
imgvMainView.contentMode = UIViewContentModeScaleAspectFit;
});
});
Problem is, when i scroll collection view some images are overlapped on one another. Please tell me solution to avoid it.
Thanks in advance.
Please use below line I hope this would work what I am assuming problem of duplicate cell.
UIImageView* imgvMainView =[[UIImageView alloc] initWithFrame:CGRectMake(34,41,177,124)];
[cell.contentView addSubview:imgvMainView];
This is most likely happening because you are reusing cells (correctly) and that the previous use of the cell is still calling the previous image. You must cancel the async task when the cell goes out of view. By the way, it will make you life much easier to use AFNetworking+UIImageView to do this.
One other option is to check that there is no UIimageView in your cell before you create one.
dispatch_sync(dispatch_get_main_queue(), ^{
UIView *viewToRemove;
for (UIView *view in cell.subViews) {
if (view isKingOfClass:UIImageView){
viewToRemove = view;
}
}
[viewToRemove removeFromSuperView];
imgvMainView =[[UIImageView alloc] initWithFrame:CGRectMake(34,41,177,124)];
[cell addSubview:imgvMainView];
[imgvMainView setImage:image];
imgvMainView.contentMode = UIViewContentModeScaleAspectFit;
});

Image not showing on preloaded UIImageView

When app launches I add UIImageViews to screen with no image. Then I load image one by one using NSThread and set it for those views. The UiImageViews remain blank until after all images have been loaded and the thread finishes. Why doesn't the image show as soon as I set it for the UIImageView, why does it wait for NSThread to end?
To add image views :
for(i = value; i < val; i++){
UIButton *newbutton = [UIButton buttonWithType:UIButtonTypeCustom];
[newbutton setBackgroundColor:[UIColor whiteColor]];
UIImageView *newback = [[UIImageView alloc] init];
newback.contentMode = UIViewContentModeScaleAspectFit;
[newback setFrame:CGRectMake(0, 0, 140, 140)];
[newbutton addSubview:newback];
height = MIN(300, newSize.height);
[newbutton setFrame:CGRectMake(currentX, currentY, width, height)];
newbutton.layer.cornerRadius = 10;
newbutton.clipsToBounds = YES;
[newbutton addTarget:self action:#selector(processButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
To add images in a thread I call a function which does following :
for(i = value; i < val; i++){
UIImage *newimage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:#“http://test.com/a.jpg”, hash]]]];
UIButton *newbutton = (UIButton*)[self.subviews objectAtIndex:i];
[((UIImageView*)[newbutton.subviews objectAtIndex:0]) newimage];
}
Generally speaking, UIKit is not thread-safe. Fetch data from URL in a background thread and set the image in the main thread.
EDIT:
In your main thread do something like
dispatch_async(someDispatchQ, ^{
[self functionToGetData];
});
and inside functionToGetData do
for(i = value; i < val; i++){
NSData *data = //get Data
UIImage *newImage = //make image from data
dispatch_async(dispatch_get_main_queue(), ^{
[imageView setImage:newImage];
});
}
This will ensure that you are using a background thread to fetch data/make image and using the main thread to set the image. This also eliminates the need to use a timer to constantly poll the background thread since the background thread automatically dispatches image-setting to the main thread as soon as an image is received.
use-
import class ImageDownloader.h & .m
link-> https://github.com/psychs/imagestore/tree/master/Classes/Libraries/ImageStore
//Now use this method to download
-(void)downloadImageWithURL:(NSString *)url{
if(self.downloder)return; //If already downloading please stay away
url=[NSString stringWithFormat:#"%#%0.0fx%0.0f/%#/%#",IMAGE_ENDPOINT,self.imgViewExhibitors.frame.size.width,self.imgViewExhibitors.frame.size.height,#"fit",[ImagePath forURLString:url]];
self.downloder=[[ImageDownloader alloc] init];
self.downloder.delegate=self;
self.downloder.indicator=self.indicator;
[self.downloder downloadImageWithURL:url];
}
-(void)imageDownloaded:(UIImage *)image forIndexPath:(NSIndexPath *)indexPath{
if(image){
//Put the downloaded image in dictionary to update while scrolling
//[self.speakerData setObject:image forKey:#"image"];
self.imgViewExhibitors.image=image;
}
}
A simple fix would be
for(i = value; i < val; i++){
UIImage *newimage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:#“http://test.com/a.jpg”, hash]]]];
UIButton *newbutton = (UIButton*)[self.subviews objectAtIndex:i];
// this ensures that the UI update (setting the image) is done on the main thread.
// This is important for the UI to update properly.
dispatch_async(dispatch_get_main_queue(), ^{
[((UIImageView*)[newbutton.subviews objectAtIndex:0]) setImage:newimage];
}
}

How to Display first 9 images in UICollectionview?

I have a image url plist with 50 image urls.Iam trying to display that in a UICollectionview but it getting too slow.Iam displaying 9 cells(images) at a time like 3x3 (3 rows and 3 columns).How can i display first 9 images at loading time and next 9 images on next scrolling .?is that possible? i tried SDWebImages for loading images ,Its not working for me.Please help me.
CODE INSIDE collectionView cellForItemAtIndexPath: ()
the code inside comment take lots of time for loading image,that code is used for loading image from document directory if image is not there then i will display in one place holder image
cell = nil;
cell = (GMMCollectionViewCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:#"test" forIndexPath:indexPath];
cell.tag = indexPath.row;
docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *path;
BOOL isImageLoaded = YES;
/////////////////////PROBLEM CODE:TAKING TOO MUCH TIME TO LOAD IMAGE //////////////////////
bookImage = [UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#/%#", docPath, [[allbooksImage objectAtIndex:indexPath.row] lastPathComponent]]];
//////////////////////////////////////////////////////////////////////////////////
if(bookImage == nil){
isImageLoaded = NO;
}
if(!isImageLoaded){
[[cell grid_image] setImage:[UIImage imageNamed:#"App-icon-144x144.png"]];
} else{
[[cell grid_image] setImage:bookImage];
}
sharedManager.bookID=bookId;
return cell;
This is what iam expecting
But iam getting like this ,
App Loading time Without any scroll (first image:3 columns are empty)
App screen after first scroll (second image:last column displaying 2 images ,rest of the part still empty )
App screen after a long scroll (third image: last colum some images are there ,rest of the columns still empty)
App screen again scroll to top,so the first image changed
You can try the below code
[[cell grid_image] setImage:nil];
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
bookImage = [UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#/%#", docPath, [[allbooksImage objectAtIndex:indexPath.row] lastPathComponent]]];
dispatch_async(dispatch_get_main_queue(), ^{
if (bookImage)
{
[[cell grid_image] setImage:bookImage];
}
else
{
[[cell grid_image] setImage:[UIImage imageNamed:#"App-icon-144x144.png"]];
}
});
});
hope it will work for you.

UIImageView not redrawing when image set to nil

I am using a UIImageView to display both still and animated images. So sometimes, I'm using .image and sometimes I'm using .animationImages. This is fine.
Whether it's static or animated, I store the UIImages's in item.frames (see below)
The problem is that I'd like to display a UIActivityIndicatorView in the center of the view when the animation frames are loading. I would like this to happen with no images or frames in there. The line that isn't doing what it's supposed to is:
[self.imageView removeFromSuperview];
As a matter of fact, setting it to another image at this point doesn't do anything either. It seems like none of the UI stuff is happening in here. BTW,
NSLog(#"%#", [NSThread isMainThread]?#"IS MAIN":#"IS NOT");
prints IS MAIN.
The image that is in there will stick around until the new animation frames are all in there (1-2 seconds) and they start animating
This is all being run from a subclass of UIView with the UIImageView as a subview.
- (void)loadItem:(StructuresItem *)item{
self.imageView.animationImages = nil;
self.imageView.image = nil;
[self.spinner startAnimating];
self.item = item;
if (item.frameCount.intValue ==1){
self.imageView.image = [item.frames objectAtIndex:0];
self.imageView.animationImages = nil;
}else {
[self.imageView removeFromSuperview];
self.imageView =[[UIImageView alloc] initWithFrame:self.bounds];
[self addSubview:self.imageView ];
if( self.imageView.isAnimating){
[self.imageView stopAnimating];
}
self.imageView.animationImages = item.frames;
self.imageView.animationDuration = self.imageView.animationImages.count/12.0f;
//if the image doesn't loop, freeze it at the end
if (!item.loop){
self.imageView.image = [self.imageView.animationImages lastObject];
self.imageView.animationRepeatCount = 1;
}
[self.imageView startAnimating];
}
[self.spinner stopAnimating];
}
My ignorant assessment is that something isn't being redrawn once that image is set to nil. Would love a hand.
What I have found is not an answer to the question, but rather a much better approach to the problem. Simply, use NSTimer instead of animationImages. It loads much faster, doesn't exhaust memory and is simpler code. yay!
Do this:
-(void)stepFrame{
self.currentFrameIndex = (self.currentFrameIndex + 1) % self.item.frames.count;
self.imageView.image = [self.item.frames objectAtIndex:self.currentFrameIndex];
}
and this
-(void)run{
if (self.item.frameCount.intValue>1){
self.imageView.image = [self.item.frames objectAtIndex:self.currentFrameIndex];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1/24.0f target:self selector:#selector(stepFrame) userInfo:nil repeats:YES];
}else{
self.imageView.image = [self.item.frames objectAtIndex:0];
}
}

Memory problems with displaying large images in UITableView

I have a system which loads alot of large images from the web and displays them in custom table cells. On older devices the memory warnings happen pretty quickly so I implemented a system of deleting some from the table to try to combat this but it didn't work well enough (lots of images were deleted affecting the UI).
So I thought I could load all the images into the device's cache and then load them from there - I've implemented SDWebImage. This is great but I still havent solved the problem of memory allocation as the images are still being displayed all the time and therefore kept in memory - causing crashes.
I think I need to implement a system which shows the images (from the cache) if the cell is being displayed and hide it if the cell is not showing - I'm just stuck at how to build such a system.
Or is this not going to work? Can you really keep the apps memory low (and stop it having memory warnings / crashing) by removing images from its table cells? Or do I just need to carry on with my earlier solution and just delete images/cells until the memory warnings stop?
Updated with code
TableViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0)
{
currentIndexPath = indexPath;
ImageTableCell *cell = (ImageTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
ImageDownloader *download = [totalDownloads objectAtIndex:[indexPath row]];
if (cell == nil)
{
cell = [[[ImageTableCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: CellIdentifier] autorelease];
}
cell.imageView.image = download.image;
return cell;
}
return nil;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
int t = [totalDownloads count];
return t;
}
ImageTableCell.m - Custom cell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self)
{
self.frame = CGRectMake(0.0f, 0.0f, 320.0f, 0.0f);
self.contentView.frame = CGRectMake(0.0f, 0.0f, 320.0f, 0.0f);
self.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth);
self.contentMode = UIViewContentModeScaleToFill;
self.autoresizesSubviews = YES;
self.contentView.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth);
self.contentView.contentMode = UIViewContentModeScaleToFill;
self.contentView.autoresizesSubviews = YES;
[self.imageView drawRect:CGRectMake(0.0f, 0.0f, 320.0f, 0.0f)];
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
self.imageView.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth);
self.imageView.opaque = YES;
}
return self;
}
ImageDownloader (implements SDWebImageManagerDelegate)
-(void) downloadImage // Comes from Model class
{
if (image == nil)
{
NSURL *url = [NSURL URLWithString:self.urlString];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
// Remove in progress downloader from queue
[manager cancelForDelegate:self];
if (url)
{
[manager downloadWithURL:url delegate:self retryFailed:YES];
}
}
}
- (void)cancelCurrentImageLoad
{
[[SDWebImageManager sharedManager] cancelForDelegate:self];
}
- (void)webImageManager:(SDWebImageManager *)imageManager didFinishWithImage:(UIImage *)_image
{
self.image = _image;
if ([self.delegate respondsToSelector:#selector(addImageToModel:)]) [self.delegate addImageToModel:self];
}
- (void)webImageManager:(SDWebImageManager *)imageManager didFailWithError:(NSError *)error;
{
if ([self.delegate respondsToSelector:#selector(badImage)]) [self.delegate badImage];
}
After you download the images, dont keep the large images in memory. just create a small size of image(thumbnail) to display in the tableview and write the larger image to some directory.
you can create a thumbnail of your image using the following code.
CGSize size = CGSizeMake(32, 32);
UIGraphicsBeginImageContext(size);
[yourImage drawInRect:CGRectMake(0, 0, 32, 32)];
yourImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Instead of using [UIImage imageNamed:#""] , try [[[UIImage alloc] initWithContentsOfFile:#""] autorelease];
Edit:
Fine. I have gone through the SDWebImage.
Use NSAutoreleasePool wherever you find that a new thread has been spawned.
And one more solution would be, resize the image before saving to cache.
So basically once the image is downloaded it stays in it's SDWebImage instance and never gets released. You should save your image to iPhone's disk with:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *imagePath = [documentsPath stringByAppendingPathComponent:[NSString stringWithFormat:#"image%d.jpg", rowNumber]; // you should have this set somewhere
[UIImageJPEGRepresentation(_image, 1.0) writeToFile:imagePath atomically:YES];
and keep just the path to it in your SDWebImage instance. Then in -cellForRowAtIndexPath: method instead of doing:
cell.imageView.image = download.image;
you should do something like:
UIImage *image = [[UIImage alloc] initwithContentsOfFile:download.imagePath];
cell.imageView.image = image;
[image release];
This will always load the image from the disk and since cell.imageView.image is a retained property, once it get's niled or reused, it will clean up the image from memory.
I would like to know how you are loading the images, are you using custom cells? If so please go through the Apple's UITableView Programming guide They are clearly saying us how to load the images. In that they are saying we should need to draw the images top avoid the memory issues.
Best example on how to load images are given in Apple's Sample Code LazyTableImages Please go through this too.
I have used kingfisher SDK and resized the server image to my custom cell size. It helps me a lot.
extension NewsCollectionViewCell {
func configure(with news: Articles) {
KF.url(URL(string:news?.urlToImage ?? ""), cacheKey: "\(news?.urlToImage ?? "")-").downsampling(size: imgNews.frame.size).cacheOriginalImage().set(to: imgNews)
}
}

Resources