Refreshing UITableViewCell after fetching image data by dispatch_async - ios

I'm writing app that shows TableView with entries that contains images.
I'm trying to fetch image by executing this line of code inside cellForRowAtIndexPath method:
cell.detailTextLabel.text = [artistData objectForKey:generesKey];
dispatch_async(backgroundQueue, ^{
NSURL *url_img = [NSURL URLWithString:[artistData objectForKey:pictureKey]];
NSData* data = [NSData dataWithContentsOfURL:
url_img];
cell.imageView.image = [UIImage imageWithData:data];
[self performSelectorOnMainThread:#selector(refreshCell:) withObject:cell waitUntilDone:YES];
});
After setting image I perform selector that contains:
-(void)refreshCell:(UITableViewCell*)cell{
[cell setNeedsDisplay];
[self.view setNeedsDisplay];
[self.tableViewOutlet setNeedsDisplay];
}
And image is not shown but when I click on cell or scroll entire list, images are shown. Why my View is not refreshing? Did I missed something?

It should be enough to call setNeedsLayout() on a cell.
In swift 4 it looks like this:
DispatchQueue.global().async {
let data = try? Data(contentsOf: URL(string: imageUrl)!)
DispatchQueue.main.async {
cell.imageView?.image = UIImage(data: data!)
cell.setNeedsLayout()
}
}

You'll could always reload the cell by calling [self.tableView reloadRowsAtIndexPaths#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
In order to prevent the infinite loop once you successfully download an image you'll want to cache the results. How long you cache is up to you.
NSCache *imageCache = [[NSCache alloc] init];
imageCache.name = #"My Image Cache";
UIImage *image = [imageCache objectForKey:url_img];
if (image) {
cell.imageView.image = image;
} else {
// Do your dispatch async to fetch the image.
// Once you get the image do
[imageCache setObject:[UIImage imageWithData:data] forKey:url_img];
}
You'll want the imageCache to be a property at the ViewController level. Don't create one each time incellForRowAtIndexPath

It might be related to interacting with the UI from a background queue. Try this:
dispatch_async(backgroundQueue, ^{
NSURL *url_img = [NSURL URLWithString:[artistData objectForKey:pictureKey]];
NSData* data = [NSData dataWithContentsOfURL:url_img];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:data];
});
});

Related

Odd threading issue with UITableView images loaded from URL

First up I'm a UI designer getting into Xcode, so please bear with me if i'm a little vague in my description here.
Basically I have a UITableView full of contacts, and each contact has a photo image to the left of the name, pulled from the web via url.. I have implemented lazy loading with the images, but when you scroll through the list, i seem to be getting odd flickering and a really 'laggy' (ie. not smooth) scroll.
I have tried playing around with the setNeedsDisplay calls (i know there's a lot of them), but it still seems to wig out.
Have been trying to google some 'best practices' for this, but not sure what's going on here... Any help would be greatly appreciated! Thanks.
Target: iOS8 in Xcode 6.1
Here's my code for cellForRowAtIndexPath:
if([peep getImage].length > 4) // if URL smaller than 4, it's probably not a valid URL
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if([peep getImage])
{
if(![imageCache objectForKey:indexPath])
{
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[peep getImage]]]];
if(image != nil) {
[imageCache setObject:image forKey:indexPath];
[cell.peepImage setImage:[imageCache objectForKey:indexPath]];
[[cell peepImage] setNeedsDisplay];
}
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[[cell peepImage] setImage:[imageCache objectForKey:indexPath]];
[[cell peepImage] setNeedsDisplay];
});
}
}
[cell.peepImage setNeedsDisplay];
});
}
else
{
cell.peepImage.image = [UIImage imageNamed: #"defaultuser.jpg"];
}
[cell setNeedsDisplay];
return cell;
}
My guess is the creation of the image on the main thread. By moving it off the main thread your problem should be fixed.
if([peep getImage].length > 4) // if URL smaller than 4, it's probably not a valid URL
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if([peep getImage])
{
if(![imageCache objectForKey:indexPath])
{
// Move image creation here
__block UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[peep getImage]]]];
dispatch_async(dispatch_get_main_queue(), ^{
if(image != nil) {
[imageCache setObject:image forKey:indexPath];
[cell.peepImage setImage:[imageCache objectForKey:indexPath]];
[[cell peepImage] setNeedsDisplay];
}
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[[cell peepImage] setImage:[imageCache objectForKey:indexPath]];
[[cell peepImage] setNeedsDisplay];
});
}
}
[cell.peepImage setNeedsDisplay];
});
}
else
{
cell.peepImage.image = [UIImage imageNamed: #"defaultuser.jpg"];
}
[cell setNeedsDisplay];
return cell;

Load many gif to UICollectionView

I am facing problem when I download gif images and add in to collection view.
gif images are download well, but when i try to scroll very very quickly it's crash
Please help. I have two solutions
/*
cellForGif.layer.borderColor = [[UIColor colorWithRed:54.0/255.f green:56.0/255.f blue:67.0/255.f alpha:1.0]CGColor];
cellForGif.layer.borderWidth = 0.7;
FLAnimatedImage __block *gifImage = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
gifImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:#"%li", (long)indexPath.row] ofType:#"gif"]]];
dispatch_async(dispatch_get_main_queue(), ^{
cellForGif.gifImage.animatedImage = gifImage;
cellForGif.linkOnGif = [self.linksArrayOnGifs objectAtIndex:indexPath.row];
//gifImage = nil;
});
});
return cellForGif;
*/
cellForGif.layer.borderColor = [[UIColor colorWithRed:54.0/255.f green:56.0/255.f blue:67.0/255.f alpha:1.0]CGColor];
cellForGif.layer.borderWidth = 0.7;
FLAnimatedImage *gifImage = nil;
gifImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:#"%li", (long)indexPath.row] ofType:#"gif"]]];
cellForGif.gifImage.animatedImage = gifImage;
cellForGif.linkOnGif = [self.linksArrayOnGifs objectAtIndex:indexPath.row];
//gifImage = nil;
return cellForGif;
You need to change your approach.
You have to load all images before going to set them in UICollectionViewCell.
Let's say create an NSArray contains all gif images. After images are loaded successfully then set them to the cell.
By observing straight from your code, I see that you load an image from the main bundle in cellForItemAtIndexPath method. So it is obvious that it will take some time very little (nano sec). But that is also considered large when there are a large amount of data in a cell.
And it is possible that line
[NSData dataWithContentsOfFile:[[NSBundle mainBundle]
will return nil when you scroll very very quickly.
Add a comment if it is still not clear.
EDIT:
Loading images in the background will not affect UI and scrolling will be smoother.
Also, put try-catch block in that method to check what you have missed.
cellForGif.layer.borderColor = [[UIColor colorWithRed:54.0/255.f green:56.0/255.f blue:67.0/255.f alpha:1.0]CGColor];
cellForGif.layer.borderWidth = 0.7;
#try {
FLAnimatedImage __block *gifImage = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
gifImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:#"%li", (long)indexPath.row] ofType:#"gif"]]];
dispatch_async(dispatch_get_main_queue(), ^{
cellForGif.gifImage.animatedImage = gifImage;
cellForGif.linkOnGif = [self.linksArrayOnGifs objectAtIndex:indexPath.row];
//gifImage = nil;
});
});
}
#catch (NSException *exception) {
NSLog(#"Exception :%#",exception.debugDescription);
}
swift extension based on Kampai's answer:
extension UIImageView {
func setGif(_ url: String) {
DispatchQueue.global(qos: .default).async {
do {
let gifData = try Data(contentsOf: URL(string: url) ?? URL(fileURLWithPath: ""))
DispatchQueue.main.async {
self.image = UIImage.sd_image(withGIFData: gifData)
}
} catch let error {
DispatchQueue.main.async {
self.image = UIImage(named: "defaultImg")// set default image here
}
print(error)
}
}
}
}
use it as:
cell.imageGif.setGif("image url")

ios: image cache and retrieval

I wish to implement caching feature for an image in iOS.
I wish to create a sample app for that, just for my understanding.
Here is what I want to do:
Get a high resolution image from internet and cache it, display it.
Now next time when this ViewController loads up, the image should be loaded from cache and not from internet.
I know how to cache an image using SDWebImage framework. I wish to try it with NSCache.
How can I implement it using NSCache? Examples are welcomed.
EDITED :
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
NSCache *cache = [self imageCache];
NSURL *url = [NSURL URLWithString:#"http://i1.ytimg.com/vi/YbT0xy_Jai0/maxresdefault.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [[UIImage alloc] initWithData:data];
NSString *imagePath = #"http://i1.ytimg.com/vi/YbT0xy_Jai0/maxresdefault.jpg";
// UIImage *image;
if (!(image = [cache objectForKey:imagePath])) { // always goes inside condition
// Cache miss, recreate image
image = [[UIImage alloc] initWithData:data];
if (image) { // Insert image in cache
[cache setObject:image forKey:imagePath]; // cache is always nil
}
}
_imgCache.image = image;
You can add image to cache using and Id:
[imageCache setObject:image forKey:#"myImage"];
Later you can retreive the image by using:
UIImage *image = [imageCache objectForKey:#"myImage"];
Entire flow will be like:
UIImage *image = [imageCache objectForKey:#"myImage"];
if (!image)
{
// download image
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:yourURL]];
if (imageData)
{
// Set image to cache
[imageCache setObject: [UIImage imageWithData:imageData] forKey:#"myImage"];
dispatch_async(dispatch_get_main_queue(), ^{
[yourImageView setImage:[UIImage imageWithData:imageData]];
});
}
});
}
else
{
// Use image from cache
[yourImageView setImage:image];
}
Refer the sample code to load image asynchronously and image caching,
https://developer.apple.com/library/ios/samplecode/LazyTableImages/Introduction/Intro.html

NSCache holds strong pointer to UIImage instantiated with imageWithData: and does not remove from memory on unload

I have a View Controller with a property galleryCache and when an image is downloaded using GCD and imageWithData: the image is added to the cache successfully with a key. However, when the view controller is dismissed it keeps strong pointers to those downloaded images causing them not to be removed from memory. Even if I use the removeAllObjects method on the cache in viewDidDisappear: memory does not clear up.
Does anyone know why this might be?
Here is the code for the method which downloads the images.
- (void)imageForFootageSize:(FootageSize)footageSize withCompletionHandler:(void (^)(UIImage *image))completionBlock
{
if (completionBlock) {
__block UIImage *image;
// Try getting local image from disk.
//
__block NSURL *imageURL = [self localURLForFootageSize:footageSize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
image = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageURL]];
dispatch_async(dispatch_get_main_queue(), ^{
if (image) {
completionBlock(image);
} else {
//
// Otherwise try getting remote image.
//
imageURL = [self remoteURLForFootageSize:footageSize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
dispatch_async(dispatch_get_main_queue(), ^{
image = [UIImage imageWithData:imageData];
if (image) {
//
// Save remote image to disk
//
NSURL *photoDirectoryURL = [Footage localURLForDirectory];
// Create the folder(s) where the photos are stored.
//
[[NSFileManager defaultManager] createDirectoryAtPath:[photoDirectoryURL path] withIntermediateDirectories:YES attributes:nil error:nil];
// Save photo
//
NSString *localPath = [[self localURLForFootageSize:footageSize] path];
[imageData writeToFile:localPath atomically:YES];
}
completionBlock(image);
});
});
}
});
});
}
}
Methods which use the above class method to fetch and process the UIImage in the completionHandler.
Method inside UICollectionViewCell subclass.
- (void)setPhoto:(Photo *)photo withImage:(UIImage *)image
{
[self setBackgroundColor:[UIColor blackColor]];
[self.imageView setBackgroundColor:[UIColor clearColor]];
if (photo && !image) {
[photo imageForFootageSize:[Footage footageSizeThatBestFitsRect:self.bounds]
withCompletionHandler:^(UIImage *image) {
if ([self.delegate respondsToSelector:#selector(galleryPhotoCollectionViewCell:didLoadImage:)]) {
[self.delegate galleryPhotoCollectionViewCell:self didLoadImage:image];
}
image = nil;
}];
}
[self.imageView setImage:image];
BOOL isPhotoAvailable = (BOOL)(image);
[self.imageView setHidden:!isPhotoAvailable];
[self.activityIndicatorView setHidden:isPhotoAvailable];
}
Method in UICollectionView data source delegate
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DIGalleryPhotoCollectionViewCell *photoCell = [collectionView dequeueReusableCellWithReuseIdentifier:photoCellIdentifier forIndexPath:indexPath];
[photoCell setDelegate:self];
Footage *footage = [self footageForIndexPath:indexPath];
Photo *photo = ([footage isKindOfClass:[Photo class]]) ? (Photo *)footage : nil;
if (photo) {
//
// Photo
//
[photoCell setPhoto:photo withImage:[self.galleryCache objectForKey:photo.footageID]];
}
return photoCell;
}
Here are the other relevant methods:
- (void)galleryPhotoCollectionViewCell:(DIGalleryPhotoCollectionViewCell *)cell didLoadImage:(UIImage *)image
{
NSIndexPath *indexPath = [self.galleryCollectionView indexPathForCell:cell];
Footage *footage = [self footageForIndexPath:indexPath];
if ([footage isKindOfClass:[Footage class]]) {
Photo *photo = (Photo *)footage;
UIImage *cachedImage = [self.galleryCache objectForKey:photo.footageID];
if (!cachedImage) {
cachedImage = image;
[self.galleryCache setObject:image forKey:photo.footageID];
}
[cell setPhoto:photo withImage:image];
}
}
And also my getter method for the NSCache property galleryCache
- (NSCache *)galleryCache
{
if (!_galleryCache) {
_galleryCache = [[NSCache alloc] init];
}
return _galleryCache;
}
EDIT
Here is a snapshot of Instruments showing the retain count history of one of the NSCache once its owner (a View Controller) is dismissed.
I'm not seeing anything obvious here, though I'd suggest putting a breakpoint where you purge the cache and make sure that's actually happening like you think it is.
If you still don't find it, you can run Allocations tool in Instruments and turn on "record reference counts" (see latter part of this answer, iOS app with ARC, find who is owner of an object), and you can find out precisely where your lingering strong reference is, at which point you can tackle the remediation.
The other obvious solution is to eliminate all of this code and use a proven image caching tool, like SDWebImage which does a lot of the memory and persistent storage caching for you. It's a pretty decent implementation.
OK, so after re examining my own code and re examining properties for the billionth x n time, it turns out my error was assigning the delegate property as a 'strong' type. Lesson learned: ALWAYS set delegates as WEAK.
I will definitely have to learn more about Instruments, however.

UIImageView Scrolling halts for a second

I am trying to use uiimage-from-animated-gif to load some GIFs into my UITableView Cells. But the problem is that everytime I try to load an image from my Cache, the cell halts a little before it scrolls.
This is the code I was using
postGif = (UIImageView *)[cell viewWithTag:104];
if ([[ImageCache sharedImageCache] DoesExist:post[#"gif"]] == true)
{
image = [[ImageCache sharedImageCache] GetImage:post[#"gif"]];
postGif.image = [UIImage animatedImageWithAnimatedGIFData:image];
}else
{
dispatch_queue_t backgroundQueue = dispatch_queue_create("cacheImage", 0);
dispatch_async(backgroundQueue, ^{
NSData *imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:post[#"gif"]]];
dispatch_async(dispatch_get_main_queue(), ^{
// Add the image to the cache
[[ImageCache sharedImageCache] AddImage:post[#"gif"] image:imageData];
NSIndexPath* ind = [NSIndexPath indexPathForRow:0 inSection:indexPath.section];
[self.feedTableView beginUpdates];
[self.feedTableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:ind] withRowAnimation:UITableViewRowAnimationNone];
[self.feedTableView endUpdates];
});
});
}
postGif.layer.cornerRadius = 2.0;
postGif.layer.masksToBounds = YES;
[cell.contentView addSubview:postGif];
I tried background queue as well. but the Image load is really slow with it.
if ([[ImageCache sharedImageCache] DoesExist:post[#"gif"]] == true)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
NSData *image = [[ImageCache sharedImageCache] GetImage:post[#"gif"]];
postGif.image = [UIImage animatedImageWithAnimatedGIFData:image];
});
}else
{
dispatch_queue_t backgroundQueue = dispatch_queue_create("cacheImage", 0);
dispatch_async(backgroundQueue, ^{
NSData *imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:post[#"gif"]]];
dispatch_async(dispatch_get_main_queue(), ^{
// Add the image to the cache
[[ImageCache sharedImageCache] AddImage:post[#"gif"] image:imageData];
NSIndexPath* ind = [NSIndexPath indexPathForRow:0 inSection:indexPath.section];
[self.feedTableView beginUpdates];
[self.feedTableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:ind] withRowAnimation:UITableViewRowAnimationNone];
[self.feedTableView endUpdates];
});
});
}
I am not sure what am I doing wrong. maybe I am not grasping a concept.
Maycoff suggested that I should not modify properties of user interface objects (like a UIImageView) from a background queue and that I need to create the instance of UIImage on a background queue, can someone please explain it to me what is it that I am not grasping?
Thanks for your help in advance.
The problem in your earlier code was that you were creating the UIImage from the GIF data on the main queue. Since decoding a GIF (especially one with many frames) can take some time, you were blocking the main thread, which is why the table view stopped scrolling momentarily each time it needed to display another image.
The problem in your later code is that you are modifying the image property of a UIImageView on a background queue. You must only modify user interface objects on the main queue.
What you need to do is create the UIImage on a background queue, then dispatch back to the main queue to update the UIImageView's image property. Thus:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
NSData *data = [[ImageCache sharedImageCache] GetImage:post[#"gif"]];
UIImage *image = [UIImage animatedImageWithAnimatedGIFData:data];
dispatch_async(dispatch_get_main_queue(), ^{
postGif.image = image;
}
});

Resources