Stop asynchronous UITableViewCell image loading when cell is not visible? - ios

I'm developing an iOS 5.0+ application with latest SDK.
This is the code I use to load images asynchronously for UITableViewCell.
- (UITableViewCell*)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ((groups != nil) && (groups.count > 0))
{
GroupCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[GroupCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
}
// Configure the cell...
Group* group = [groups objectAtIndex:indexPath.row];
cell.GroupNameLabel.text = group.Name;
// TODO: Poner el estado.
if (group.Photo)
cell.GroupImageView.image = group.Photo;
else
{
// download the photo asynchronously
NSString *urlString =
[NSString stringWithFormat:kGetGroupImageURL, [group.GroupId intValue]];
NSURL *url = [NSURL URLWithString:urlString];
[ImageTool downloadImageWithURL:url completionBlock:^(BOOL succeeded, UIImage *image) {
if (succeeded)
{
// change the image in the cell
cell.GroupImageView.image = image;
// cache the image for use later (when scrolling up)
group.Photo = image;
}
}];
}
return cell;
}
else
return nil;
}
And the loader:
#import "ImageTool.h"
#implementation ImageTool
+ (void)downloadImageWithURL:(NSURL *)url
completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if ( !error )
{
UIImage *image = [[UIImage alloc] initWithData:data];
completionBlock(YES,image);
} else{
completionBlock(NO,nil);
}
}];
}
But it seems it's not going to work because I don't handle if the cell for which I'm loading the image it is still visible.
How can I handle if the cell is still visible?
I have found this article, but I don't know how to implement it.

You generally handle this by checking to see if the cell is still visible using the UITableView method cellForRowAtIndexPath:, which returns nil if the cell for that index path is no longer visible. Note, this method is not to be confused with the similarly named UITableViewDataSource method tableView:cellForRowAtIndexPath:.
if (group.Photo)
cell.GroupImageView.image = group.Photo;
else
{
// don't forget to `nil` or use placeholder for `GroupImageView` because if
// the cell is reused, you might see the old image for the other row momentarily
// while the new image is being retrieved
cell.GroupImageView.image = [UIImage imageNamed:#"placeholder.png"];
// download the photo asynchronously
NSString *urlString =
[NSString stringWithFormat:kGetGroupImageURL, [group.GroupId intValue]];
NSURL *url = [NSURL URLWithString:urlString];
[ImageTool downloadImageWithURL:url completionBlock:^(BOOL succeeded, UIImage *image) {
if (succeeded)
{
dispatch_async(dispatch_get_main_queue(), ^ {
GroupCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (updateCell)
{
// change the image in the cell
updateCell.GroupImageView.image = image;
}
// cache the image for use later (when scrolling up)
group.Photo = image;
});
}
}];
}
Note, if the completion block of sendAsynchronousRequest is not submitted to the main queue (e.g. you may do something computationally expensive that you don't want to tie up the main queue with), you might further dispatch the UI update to the main queue as shown above. You can do it either here, like shown above, or in the downloadImageWithURL. But make sure to update the UI on the main queue (and to avoid synchronization issues, update group, there, too). Since you've specified the main queue for your operation queue, though, this is not a critical issue.
And as I point out in my code comments, if you're retrieving an image for a cell that has been reused, don't forget to reset the GroupImageView.image property to nil, too. If you don't, you may see the previous image for the reused cell show up momentarily while the request for the new row is in progress.
Alternatively, you should use a UIImageView category, such as provided by SDWebImage which takes care of all of this for you (including cache management).
As an aside, it also handles a subtle issue, which your code doesn't, namely where you scroll really quickly through a long tableview on a slow internet connection. You implementation wouldn't load the images for the currently visible cells until all of the other cells' images have loaded (since there can be only five concurrent NSURLConnection objects at a time). These UIImageView categories generally cancel the old requests for cells that have been reused, which ensures that the UI is presenting the images that the user is actually looking at more quickly.

Related

UICollectionView cell image error

I am developing an app with a sample UICollectionView that should load a bunch of icons that it downloads from the internet. So basically the server retrieves a list with the URLs of each icon inside a JSON file, the app parses it, and then each cell downloads the corresponding image.
The problem with this approach seems to be that if the user starts scrolling while the images are downloading, the user will start seeing the images in the wrong order! It's like UICollectionView is trying to 'help me' and it renders the content it's got in the wrong place!
I've seen many threads about this, I tried out most of their suggestions but with out much luck so far. Anybody seen something like this?
This is how the cellForItemAtIndexPath looks like:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
PeqImage *image = [self.responseIcons objectAtIndex:indexPath.row];
PeqCustomCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"PeqCustomCell" forIndexPath:indexPath];
cell.peqImage = image;
cell.imageName = [NSString stringWithFormat:#"%#.png",image.assetId];
NSString *theImageUrl = [NSString stringWithFormat:#"http://www.XXXX.com/pb/images/uncompressed/%ld.png",(long)indexPath.row];
[cell downloadImage:[NSURL URLWithString:theImageUrl]];
return cell;
}
And this is how the download algorithm looks like (using UIImageView+AFNetworking):
- (void) downloadImageWithUrl:(NSURL*) url completion:(PeqCustomCellCompletionBlock)completionBlock
{UIImage *placeholder = [UIImage imageNamed:#"placeholder"];
NSURLRequest *jpegURLRequest = [NSURLRequest requestWithURL:url];
self.imageUrl = url.absoluteString;
[self.imageView setImageWithURLRequest:jpegURLRequest
placeholderImage:placeholder
success:^(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image) {
image = [self normalizeImage:image];
completionBlock(image);
}
failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) {
NSLog(#"Error downloading image from network");
}];
}
You should use SDWebImage library to handle this kind of stuff something like,
#import <SDWebImage/UIImageView+WebCache.h>
..
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
}
// Here we use the new provided sd_setImageWithURL: method to load the web image
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
cell.textLabel.text = #"My Text";
return cell;
}
here is the example of tableview you can use same for your collection view!!
this library cache the images for reusability.
If you want to implement or know native way of this stuff then you can refer Loading Images in UICollectionViewCell: Naive to the Clever.
I suspect threads as being the culprit. All UI code must be called from the main thread. These days with so many blocks in use, it's easy to accidentally make UI calls from a background thread. When you do this, all manner of weirdness begins to materialize.
I'd try changing your last snippet of code to this, so that the image calls get dispatched on the main thread. I've just added a dispatch_async() function.
self.imageUrl = url.absoluteString;
[self.imageView setImageWithURLRequest:jpegURLRequest
placeholderImage:placeholder
success:^(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image){
dispatch_async(dispatch_get_main_queue(), ^{
image = [self normalizeImage:image];
completionBlock(image);
});
}
failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) {
NSLog(#"Error downloading image from network");
}];
The problem is that collection view cells are reused. So you're' telling a cell to download a particular image while its onscreen, and then it goes offscreen and gets reused when the collection view needs another cell, and then you're telling the SAME cell to download another image. Whichever image finishes downloading last is the last one that will be displayed.
In your download completion block, before setting image = [self normalizeImage:image];, you should check if the request url matches the current self.imageUrl. If it doesn't, then the image is from an old request and you don't want to display it in the cell.
The issue is that you re-download the image every time the cell is hidden and shown again. The better method is to download all the data in advance and then populate the collectionView using that.

Which is the best way to cache the data from API's in swift? [duplicate]

Shortly, I have an NSDictionary with urls for images that I need to show in my UITableView. Each cell has a title and an image. I had successfully made this happen, although the scrolling was lagging, as it seemed like the cells downloaded their image every time they came into the screen.
I searched for a bit, and found SDWebImage on github. This made the scroll-lagg go away. I am not completely sure what it did, but I believed it did some caching.
But! Every time I open the app for the first time, I see NO images, and I have to scroll down, and back up for them to arrive. And if I exit the app with home-button, and open again, then it seemes like the caching is working, because the images on the screen are visible, however, if I scroll one cell down, then the next cell has no image. Until i scroll past it and back up, or if I click on it. Is this how caching is supposed to work? Or what is the best way to cache images downloaded from the web? The images are being updated rarily, so I was close to just import them to the project, but I like to have the possibility to update images without uploading an update..
Is it impossible to load all the images for the whole tableview form the cache(given that there is something in the cache) at launch? Is that why I sometimes see cells without images?
And yes, I'm having a hard time understanding what cache is.
--EDIT--
I tried this with only images of the same size (500x150), and the aspect-error is gone, however when I scroll up or down, there are images on all cells, but at first they are wrong. After the cell has been in the view for some milliseconds, the right image appears. This is amazingly annoying, but maybe how it has to be?.. It seemes like it chooses the wrong index from the cache at first. If I scroll slow, then I can see the images blink from wrong image to the correct one. If I scroll fast, then I believe the wrong images are visible at all times, but I can't tell due to the fast scrolling. When the fast scrolling slows down and eventually stops, the wrong images still appear, but immediately after it stops scrolling, it updates to the right images. I also have a custom UITableViewCell class, but I haven't made any big changes.. I haven't gone through my code very much yet, but I can't think of what may be wrong.. Maybe I have something in the wrong order.. I have programmed much in java, c#, php etc, but I'm having a hard time understanding Objective-c, with all the .h and .m ...
I have also `
#interface FirstViewController : UITableViewController{
/**/
NSCache *_imageCache;
}
(among other variables) in FirstViewController.h. Is this not correct?
Here's my cellForRowAtIndexPath.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"hallo";
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[CustomCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSMutableArray *marr = [hallo objectAtIndex:indexPath.section];
NSDictionary *dict = [marr objectAtIndex:indexPath.row];
NSString* imageName = [dict objectForKey:#"Image"];
//NSLog(#"url: %#", imageURL);
UIImage *image = [_imageCache objectForKey:imageName];
if(image)
{
cell.imageView.image = image;
}
else
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString* imageURLString = [NSString stringWithFormat:#"example.com/%#", imageName];
NSURL *imageURL = [NSURL URLWithString:imageURLString];
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:imageURL]];
if(image)
{
dispatch_async(dispatch_get_main_queue(), ^{
CustomCell *cell =(CustomCell*)[self.tableView cellForRowAtIndexPath:indexPath];
if(cell)
{
cell.imageView.image = image;
}
});
[_imageCache setObject:image forKey:imageName];
}
});
}
cell.textLabel.text = [dict objectForKey:#"Name"];
return cell;
}
Caching just means keeping a copy of the data that you need so that you don't have to load it from some slower source. For example, microprocessors often have cache memory where they keep copies of data so that they don't have to access RAM, which is a lot slower. Hard disks often have memory caches from which the file system can get much quicker access to blocks of data that have been accessed recently.
Similarly, if your app loads a lot of images from the network, it may be in your interest to cache them on your device instead of downloading them every time you need them. There are lots of ways to do that -- it sounds like you already found one. You might want to store the images you download in your app's /Library/Caches directory, especially if you don't expect them to change. Loading the images from secondary storage will be much, much quicker than loading them over the network.
You might also be interested in the little-known NSCache class for keeping the images you need in memory. NSCache works like a dictionary, but when memory gets tight it'll start releasing some of its contents. You can check the cache for a given image first, and if you don't find it there you can then look in your caches directory, and if you don't find it there you can download it. None of this will speed up image loading on your app the first time you run it, but once your app has downloaded most of what it needs it'll be much more responsive.
I think Caleb answered the caching question well. I was just going to touch upon the process for updating your UI as you retrieve images, e.g. assuming you have a NSCache for your images called _imageCache:
First, define an operation queue property for the tableview:
#property (nonatomic, strong) NSOperationQueue *queue;
Then in viewDidLoad, initialize this:
self.queue = [[NSOperationQueue alloc] init];
self.queue.maxConcurrentOperationCount = 4;
And then in cellForRowAtIndexPath, you could then:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"ilvcCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// set the various cell properties
// now update the cell image
NSString *imagename = [self imageFilename:indexPath]; // the name of the image being retrieved
UIImage *image = [_imageCache objectForKey:imagename];
if (image)
{
// if we have an cachedImage sitting in memory already, then use it
cell.imageView.image = image;
}
else
{
cell.imageView.image = [UIImage imageNamed:#"blank_cell_image.png"];
// the get the image in the background
[self.queue addOperationWithBlock:^{
// get the UIImage
UIImage *image = [self getImage:imagename];
// if we found it, then update UI
if (image)
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// if the cell is visible, then set the image
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell)
cell.imageView.image = image;
}];
[_imageCache setObject:image forKey:imagename];
}
}];
}
return cell;
}
I only mention this as I've seen a few code samples floating around on SO recently that use GCD to update the appropriate UIImageView image property, but in the process of dispatching the UI update back to the main queue, they employ curious techniques (e.g., reloading the cell or table, just updating the image property of the existing cell object returned at the top of the tableView:cellForRowAtIndexPath (which is a problem if the row has scrolled off the screen and the cell has been dequeued and is being reused for a new row), etc.). By using cellForRowAtIndexPath (not to be confused with tableView:cellForRowAtIndexPath), you can determine if the cell is still visible and/or if it may have scrolled off and been dequeued and reused.
The simplest solution is to go with something heavily used that has been stress tested.
SDWebImage is a powerful tool that helped me solve a similar problem and can easily be installed w/ cocoa pods. In podfile:
platform :ios, '6.1'
pod 'SDWebImage', '~>3.6'
Setup cache:
SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:#"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image)
{
// image is not nil if image was found
}];
Cache image:
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
https://github.com/rs/SDWebImage
I think will be better for you user something like DLImageLoader.
More info -> https://github.com/AndreyLunevich/DLImageLoader-iOS
[[DLImageLoader sharedInstance] loadImageFromUrl:#"image_url_here"
completed:^(NSError *error, UIImage *image) {
if (error == nil) {
imageView.image = image;
} else {
// if we got an error when load an image
}
}];
For the part of the question about wrong images, it's because of the reuse of cells. Reuse of cells means that the existing cells, which go out of view (for example, the cells which go out of the screen in the top when you scroll towards the bottom are the ones coming back again from the bottom.) And so you get incorrect images. But once the cell shows up, the code for fetching the proper image executes and you get the proper images.
You can use a placeholder in 'prepareForReuse' method of the cell. This function is mostly used when you need to reset the values when the cell is brought up for reuse. Setting a placeholder here will make sure you won't get any incorrect images.
Caching images can be done as simply as this.
ImageService.m
#implementation ImageService{
NSCache * Cache;
}
const NSString * imageCacheKeyPrefix = #"Image-";
-(id) init {
self = [super init];
if(self) {
Cache = [[NSCache alloc] init];
}
return self;
}
/**
* Get Image from cache first and if not then get from server
*
**/
- (void) getImage: (NSString *) key
imagePath: (NSString *) imagePath
completion: (void (^)(UIImage * image)) handler
{
UIImage * image = [Cache objectForKey: key];
if( ! image || imagePath == nil || ! [imagePath length])
{
image = NOIMAGE; // Macro (UIImage*) for no image
[Cache setObject:image forKey: key];
dispatch_async(dispatch_get_main_queue(), ^(void){
handler(image);
});
}
else
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0ul ),^(void){
UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[imagePath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]];
if( !image)
{
image = NOIMAGE;
}
[Cache setObject:image forKey: key];
dispatch_async(dispatch_get_main_queue(), ^(void){
handler(image);
});
});
}
}
- (void) getUserImage: (NSString *) userId
completion: (void (^)(UIImage * image)) handler
{
[self getImage: [NSString stringWithFormat: #"%#user-%#", imageCacheKeyPrefix, userId]
imagePath: [NSString stringWithFormat: #"http://graph.facebook.com/%#/picture?type=square", userId]
completion: handler];
}
SomeViewController.m
[imageService getUserImage: userId
completion: ^(UIImage *image) {
annotationImage.image = image;
}];
////.h file
#import <UIKit/UIKit.h>
#interface UIImageView (KJ_Imageview_WebCache)
-(void)loadImageUsingUrlString :(NSString *)urlString placeholder :(UIImage *)placeholder_image;
#end
//.m file
#import "UIImageView+KJ_Imageview_WebCache.h"
#implementation UIImageView (KJ_Imageview_WebCache)
-(void)loadImageUsingUrlString :(NSString *)urlString placeholder :(UIImage *)placeholder_image
{
NSString *imageUrlString = urlString;
NSURL *url = [NSURL URLWithString:urlString];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *getImagePath = [documentsDirectory stringByAppendingPathComponent:[self tream_char:urlString]];
NSLog(#"getImagePath--->%#",getImagePath);
UIImage *customImage = [UIImage imageWithContentsOfFile:getImagePath];
if (customImage)
{
self.image = customImage;
return;
}
else
{
self.image=placeholder_image;
}
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *uploadTask = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error)
{
NSLog(#"%#",[error localizedDescription]);
self.image=placeholder_image;
return ;
}
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *imageToCache = [UIImage imageWithData:data];
if (imageUrlString == urlString)
{
self.image = imageToCache;
}
[self saveImage:data ImageString:[self tream_char:urlString]];
});
}];
[uploadTask resume];
}
-(NSString *)tream_char :(NSString *)string
{
NSString *unfilteredString =string;
NSCharacterSet *notAllowedChars = [[NSCharacterSet characterSetWithCharactersInString:#"!##$%^&*()_+|abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"] invertedSet];
NSString *resultString = [[unfilteredString componentsSeparatedByCharactersInSet:notAllowedChars] componentsJoinedByString:#""];
NSLog (#"Result: %#", resultString);
return resultString;
}
-(void)saveImage : (NSData *)Imagedata ImageString : (NSString *)imageString
{
NSArray* documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES);
NSString* documentDirectory = [documentDirectories objectAtIndex:0];
NSString* documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent:imageString];
if (![Imagedata writeToFile:documentDirectoryFilename atomically:NO])
{
NSLog((#"Failed to cache image data to disk"));
}
else
{
NSLog(#"the cachedImagedPath is %#",documentDirectoryFilename);
}
}
#end
/// call
[cell.ProductImage loadImageUsingUrlString:[[ArrProductList objectAtIndex:indexPath.row] valueForKey:#"product_image"] placeholder:[UIImage imageNamed:#"app_placeholder"]];

Custom Cell images are changing on their own in a UITableView [duplicate]

This question already has answers here:
GCD UITableView asynchronous load images, wrong cells are loaded until new image download
(3 answers)
Closed 7 years ago.
I have a table view with custom cells. Every cell has an image, title, and a description. When I first load the table, it loads fine. if I slowly scroll trough the images, also seems to work fine. As soon as I scroll down fast (assuming the number of cells is large enough to not fit in without scrolling up and down) the images start to changes cells in a random order. Some cells have the same image twice.
Any clue why this is happening?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
BookmarkCellViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NewsArticle *currArt = [self.lN_Dept objectAtIndex:indexPath.row];
if(currArt.artImage == Nil)
{
if([currArt.mainImage_URL rangeOfString:#"<img src="].location != NSNotFound)
{
NSRange range = [currArt.mainImage_URL rangeOfString:#"<img src=\"/CONC/"];
NSString *substring = [[currArt.mainImage_URL substringFromIndex:NSMaxRange(range)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSRange range2 = [substring rangeOfString:#"\""];
NSString *substring2 = [[substring substringToIndex:NSMaxRange(range2)-1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString *imageURL = [PUB_URL stringByAppendingString:substring2];
[self downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image) {
if (succeeded) {
currArt.artImage = image;
cell.ArtDisplayImage.image = currArt.artImage;
}
}];
}
}
else
{
cell.ArtDisplayImage.image = currArt.artImage;
}
[cell configureCellForEntry:currArticle];
return cell;
}
- (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *Data, NSError *error) {
if ( !error )
{
UIImage *image = [[UIImage alloc] initWithData:Data];
completionBlock(YES,image);
} else{
completionBlock(NO,nil);
}
}];
}
It happens because you misunderstand how UITableView works. Let's imagine your table view has 100 cells and it can display 10 cells simultaneously. When table view loads, it creates 10 instances of your cells. When you start scrolling down through the table view it actually doesn't creates new instances of your cells – it reuses cells that have disappeared from the screen (because you are calling [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];). That means that the cell at index 10 will have the same reference that the cell at index 0.
Back to your question you are loading images with
[self downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image) {
if (succeeded) {
currArt.artImage = image;
cell.ArtDisplayImage.image = currArt.artImage;
}
}];
In case the image was downloaded faster then the cell was reused (which happens when your are scrolling slowly) everything will be alright. But in case cell was reused before image was downloaded then there are two blocks in memory that are downloading images for that cell. To solve this issue I would recommend you to use SDWebImage which handles this situations automatically or cancel downloading of images for cells which are disappeared from the screen. Hope this will help.
There's probably a race condition here. A download is queued up for a given cell and then that cell is reused and another image is set or download is queued but then the first download completes and sets the image even though the cell represents a different element now. If you want to see if this is indeed the issue, disable cell reuse (instantiate a new cell for every row). If the problem goes away, then that was it.
One thing you can do is cancel an existing download that was started by a given cell if that cell is reused. Check out the open source library SDWebImage which handles this issue this way.
Another thing you can do is have the cell store the URL it is currently trying to load and compare the URL of the resulting download against the cell's stored URL to see if they are still the same (and only set the image if they are still the same).
Something like this should do it:
if(currArt.artImage == Nil)
{
if([currArt.mainImage_URL rangeOfString:#"<img src="].location != NSNotFound)
{
NSRange range = [currArt.mainImage_URL rangeOfString:#"<img src=\"/CONC/"];
NSString *substring = [[currArt.mainImage_URL substringFromIndex:NSMaxRange(range)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSRange range2 = [substring rangeOfString:#"\""];
NSString *substring2 = [[substring substringToIndex:NSMaxRange(range2)-1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString *imageURL = [PUB_URL stringByAppendingString:substring2];
cell.imageURL = imageURL; // you need to add an imageURL string property to your cells
// you need to return the URL string in your block to compare it
[self downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image, NSString *url)
{
if (succeeded)
{
currArt.artImage = image;
if(cell.imageURL isEqualToString:url])
{
cell.ArtDisplayImage.image = currArt.artImage;
}
}
}];
}
}
else
{
cell.ArtDisplayImage.image = currArt.artImage;
cell.imageURL = nil; // make sure it is not overwritten
}
So something like this:
[self downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image) {
if (succeeded) {
YourCellType *cellToUpdate = [tableView cellForIndexPath:indexPath];
if (cellToUpdate)
{
currArt.artImage = image;
cellToUpdate.ArtDisplayImage.image = currArt.artImage;
}
}
}];

UICollectionView - Downloading images and effective reloading of data, like Instagram

I noticed that apps like Intagram uses UICollectionViews to display the feed of photos.
I also noticed that the cells for these photos is somehow placed on screen before the actual photos are downloaded completely. Then when a download completes, that photo is nicely displayed in the correct cell.
I would like to copy that functionality, but I do not know how to go about it.
I am currently downloading a large JSON object which I transform to an array of NSDictionaries. Each NSDictionary contains information about each photo, and among that information, an URL is presented. At this URL, I can find the corresponding image that I need to download and display in my UICollectionViewCells
As for now, I iterate this list and initiate a download for each URL I see. When that download is complete, I reload the collectionview using [self.collectionView reloadData]. But, as you can imagine, if I have 30 cells that all wants an image, there is a lot of reloadData calls.
I am using AFNetworking to handle the download, here is the method, which I call based on the URL I mentioned before:
-(void) downloadFeedImages:(NSString *) photoURL imageDescs:(NSDictionary*)imageDescs photoId:(NSString *)photoID{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *directory = [paths objectAtIndex:0];
NSString* foofile = [directory stringByAppendingPathComponent:photoID];
if([[NSFileManager defaultManager] fileExistsAtPath:foofile]){
// IF IMAGE IS CACHED
[self.collectionView reloadData];
return;
}
NSLog(#"photoURL: %#", photoURL);
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:photoURL]];
AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request
imageProcessingBlock:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
// Save Image
NSLog(#"URL-RESPONSE:%#", image);
NSString *myFile = [directory stringByAppendingPathComponent:photoID];
NSData *imageData = UIImagePNGRepresentation(image);
[imageData writeToFile:myFile atomically: YES];
[self.collectionView reloadData];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"ERROR: %#", [error localizedDescription]);
}];
[[WebAPI sharedInstance] enqueueHTTPRequestOperation:operation];
}
So basically, I wonder how I can achieve the functionality that Instagram and similar applications has when it comes to displaying a feed with images.
In addition, I would like to know a good way to initiate a download for each cell, and when that download is finished, update that cell, not redraw the entire view using [reloadData]
Thanks
The technique you want to implement is called lazy loading. Since you are using AFNetworking it will be easier to implement this in your case. Each of your collection view cell needs to have a UIImageView to display the image. Use the UIImageView+AFNetworking.h category and set the correct image URL by calling method
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
// ....
[cell.imageView setImageWithURL:imageURL placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
// ...
return cell;
}
Placeholder is the image which will be displayed until required image is downloaded. This will simply do the required job for you.
Note: By default, URL requests have a cache policy of NSURLCacheStorageAllowed and a timeout interval of 30 seconds, and are set not handle cookies. To configure URL requests differently, use setImageWithURLRequest:placeholderImage:success:failure:.
Also, for you reference, if you want to implement lazy loading of images yourself, follow this Apple sample code. This is for UITableView but same technique can be used for UICollectionView as well.
Hope that helps!
use
[self.collectionView reloadItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
instead of
[self.collectionView reloadData];
here indexPath is the NSIndexPath Object for the corresponding UICollectionViewCell Object
Use following to load image Async in cell in cellForItemAtIndexPath delegate method
let url = URL(string: productModel.productImage)
let data = try? Data(contentsOf: url!)
DispatchQueue.main.async(execute: {
collectionViewCell.imageView.image = UIImage(data: data!)
});
Implement protocol "UICollectionViewDataSourcePrefetching" in you ViewController as
class ViewController: UIViewController , UICollectionViewDataSourcePrefetching {
Set following delegates to your collection view in storyboard (see the attached image)
or programmatically
In ViewController's viewDidLoad method
collectionView.delegate = self
collectionView.dataSource = self
collectionView.prefetchDataSource = self

Best way to cache images on ios app?

Shortly, I have an NSDictionary with urls for images that I need to show in my UITableView. Each cell has a title and an image. I had successfully made this happen, although the scrolling was lagging, as it seemed like the cells downloaded their image every time they came into the screen.
I searched for a bit, and found SDWebImage on github. This made the scroll-lagg go away. I am not completely sure what it did, but I believed it did some caching.
But! Every time I open the app for the first time, I see NO images, and I have to scroll down, and back up for them to arrive. And if I exit the app with home-button, and open again, then it seemes like the caching is working, because the images on the screen are visible, however, if I scroll one cell down, then the next cell has no image. Until i scroll past it and back up, or if I click on it. Is this how caching is supposed to work? Or what is the best way to cache images downloaded from the web? The images are being updated rarily, so I was close to just import them to the project, but I like to have the possibility to update images without uploading an update..
Is it impossible to load all the images for the whole tableview form the cache(given that there is something in the cache) at launch? Is that why I sometimes see cells without images?
And yes, I'm having a hard time understanding what cache is.
--EDIT--
I tried this with only images of the same size (500x150), and the aspect-error is gone, however when I scroll up or down, there are images on all cells, but at first they are wrong. After the cell has been in the view for some milliseconds, the right image appears. This is amazingly annoying, but maybe how it has to be?.. It seemes like it chooses the wrong index from the cache at first. If I scroll slow, then I can see the images blink from wrong image to the correct one. If I scroll fast, then I believe the wrong images are visible at all times, but I can't tell due to the fast scrolling. When the fast scrolling slows down and eventually stops, the wrong images still appear, but immediately after it stops scrolling, it updates to the right images. I also have a custom UITableViewCell class, but I haven't made any big changes.. I haven't gone through my code very much yet, but I can't think of what may be wrong.. Maybe I have something in the wrong order.. I have programmed much in java, c#, php etc, but I'm having a hard time understanding Objective-c, with all the .h and .m ...
I have also `
#interface FirstViewController : UITableViewController{
/**/
NSCache *_imageCache;
}
(among other variables) in FirstViewController.h. Is this not correct?
Here's my cellForRowAtIndexPath.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"hallo";
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[CustomCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSMutableArray *marr = [hallo objectAtIndex:indexPath.section];
NSDictionary *dict = [marr objectAtIndex:indexPath.row];
NSString* imageName = [dict objectForKey:#"Image"];
//NSLog(#"url: %#", imageURL);
UIImage *image = [_imageCache objectForKey:imageName];
if(image)
{
cell.imageView.image = image;
}
else
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString* imageURLString = [NSString stringWithFormat:#"example.com/%#", imageName];
NSURL *imageURL = [NSURL URLWithString:imageURLString];
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:imageURL]];
if(image)
{
dispatch_async(dispatch_get_main_queue(), ^{
CustomCell *cell =(CustomCell*)[self.tableView cellForRowAtIndexPath:indexPath];
if(cell)
{
cell.imageView.image = image;
}
});
[_imageCache setObject:image forKey:imageName];
}
});
}
cell.textLabel.text = [dict objectForKey:#"Name"];
return cell;
}
Caching just means keeping a copy of the data that you need so that you don't have to load it from some slower source. For example, microprocessors often have cache memory where they keep copies of data so that they don't have to access RAM, which is a lot slower. Hard disks often have memory caches from which the file system can get much quicker access to blocks of data that have been accessed recently.
Similarly, if your app loads a lot of images from the network, it may be in your interest to cache them on your device instead of downloading them every time you need them. There are lots of ways to do that -- it sounds like you already found one. You might want to store the images you download in your app's /Library/Caches directory, especially if you don't expect them to change. Loading the images from secondary storage will be much, much quicker than loading them over the network.
You might also be interested in the little-known NSCache class for keeping the images you need in memory. NSCache works like a dictionary, but when memory gets tight it'll start releasing some of its contents. You can check the cache for a given image first, and if you don't find it there you can then look in your caches directory, and if you don't find it there you can download it. None of this will speed up image loading on your app the first time you run it, but once your app has downloaded most of what it needs it'll be much more responsive.
I think Caleb answered the caching question well. I was just going to touch upon the process for updating your UI as you retrieve images, e.g. assuming you have a NSCache for your images called _imageCache:
First, define an operation queue property for the tableview:
#property (nonatomic, strong) NSOperationQueue *queue;
Then in viewDidLoad, initialize this:
self.queue = [[NSOperationQueue alloc] init];
self.queue.maxConcurrentOperationCount = 4;
And then in cellForRowAtIndexPath, you could then:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"ilvcCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// set the various cell properties
// now update the cell image
NSString *imagename = [self imageFilename:indexPath]; // the name of the image being retrieved
UIImage *image = [_imageCache objectForKey:imagename];
if (image)
{
// if we have an cachedImage sitting in memory already, then use it
cell.imageView.image = image;
}
else
{
cell.imageView.image = [UIImage imageNamed:#"blank_cell_image.png"];
// the get the image in the background
[self.queue addOperationWithBlock:^{
// get the UIImage
UIImage *image = [self getImage:imagename];
// if we found it, then update UI
if (image)
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// if the cell is visible, then set the image
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell)
cell.imageView.image = image;
}];
[_imageCache setObject:image forKey:imagename];
}
}];
}
return cell;
}
I only mention this as I've seen a few code samples floating around on SO recently that use GCD to update the appropriate UIImageView image property, but in the process of dispatching the UI update back to the main queue, they employ curious techniques (e.g., reloading the cell or table, just updating the image property of the existing cell object returned at the top of the tableView:cellForRowAtIndexPath (which is a problem if the row has scrolled off the screen and the cell has been dequeued and is being reused for a new row), etc.). By using cellForRowAtIndexPath (not to be confused with tableView:cellForRowAtIndexPath), you can determine if the cell is still visible and/or if it may have scrolled off and been dequeued and reused.
The simplest solution is to go with something heavily used that has been stress tested.
SDWebImage is a powerful tool that helped me solve a similar problem and can easily be installed w/ cocoa pods. In podfile:
platform :ios, '6.1'
pod 'SDWebImage', '~>3.6'
Setup cache:
SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:#"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image)
{
// image is not nil if image was found
}];
Cache image:
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
https://github.com/rs/SDWebImage
I think will be better for you user something like DLImageLoader.
More info -> https://github.com/AndreyLunevich/DLImageLoader-iOS
[[DLImageLoader sharedInstance] loadImageFromUrl:#"image_url_here"
completed:^(NSError *error, UIImage *image) {
if (error == nil) {
imageView.image = image;
} else {
// if we got an error when load an image
}
}];
For the part of the question about wrong images, it's because of the reuse of cells. Reuse of cells means that the existing cells, which go out of view (for example, the cells which go out of the screen in the top when you scroll towards the bottom are the ones coming back again from the bottom.) And so you get incorrect images. But once the cell shows up, the code for fetching the proper image executes and you get the proper images.
You can use a placeholder in 'prepareForReuse' method of the cell. This function is mostly used when you need to reset the values when the cell is brought up for reuse. Setting a placeholder here will make sure you won't get any incorrect images.
Caching images can be done as simply as this.
ImageService.m
#implementation ImageService{
NSCache * Cache;
}
const NSString * imageCacheKeyPrefix = #"Image-";
-(id) init {
self = [super init];
if(self) {
Cache = [[NSCache alloc] init];
}
return self;
}
/**
* Get Image from cache first and if not then get from server
*
**/
- (void) getImage: (NSString *) key
imagePath: (NSString *) imagePath
completion: (void (^)(UIImage * image)) handler
{
UIImage * image = [Cache objectForKey: key];
if( ! image || imagePath == nil || ! [imagePath length])
{
image = NOIMAGE; // Macro (UIImage*) for no image
[Cache setObject:image forKey: key];
dispatch_async(dispatch_get_main_queue(), ^(void){
handler(image);
});
}
else
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0ul ),^(void){
UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[imagePath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]];
if( !image)
{
image = NOIMAGE;
}
[Cache setObject:image forKey: key];
dispatch_async(dispatch_get_main_queue(), ^(void){
handler(image);
});
});
}
}
- (void) getUserImage: (NSString *) userId
completion: (void (^)(UIImage * image)) handler
{
[self getImage: [NSString stringWithFormat: #"%#user-%#", imageCacheKeyPrefix, userId]
imagePath: [NSString stringWithFormat: #"http://graph.facebook.com/%#/picture?type=square", userId]
completion: handler];
}
SomeViewController.m
[imageService getUserImage: userId
completion: ^(UIImage *image) {
annotationImage.image = image;
}];
////.h file
#import <UIKit/UIKit.h>
#interface UIImageView (KJ_Imageview_WebCache)
-(void)loadImageUsingUrlString :(NSString *)urlString placeholder :(UIImage *)placeholder_image;
#end
//.m file
#import "UIImageView+KJ_Imageview_WebCache.h"
#implementation UIImageView (KJ_Imageview_WebCache)
-(void)loadImageUsingUrlString :(NSString *)urlString placeholder :(UIImage *)placeholder_image
{
NSString *imageUrlString = urlString;
NSURL *url = [NSURL URLWithString:urlString];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *getImagePath = [documentsDirectory stringByAppendingPathComponent:[self tream_char:urlString]];
NSLog(#"getImagePath--->%#",getImagePath);
UIImage *customImage = [UIImage imageWithContentsOfFile:getImagePath];
if (customImage)
{
self.image = customImage;
return;
}
else
{
self.image=placeholder_image;
}
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *uploadTask = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error)
{
NSLog(#"%#",[error localizedDescription]);
self.image=placeholder_image;
return ;
}
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *imageToCache = [UIImage imageWithData:data];
if (imageUrlString == urlString)
{
self.image = imageToCache;
}
[self saveImage:data ImageString:[self tream_char:urlString]];
});
}];
[uploadTask resume];
}
-(NSString *)tream_char :(NSString *)string
{
NSString *unfilteredString =string;
NSCharacterSet *notAllowedChars = [[NSCharacterSet characterSetWithCharactersInString:#"!##$%^&*()_+|abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"] invertedSet];
NSString *resultString = [[unfilteredString componentsSeparatedByCharactersInSet:notAllowedChars] componentsJoinedByString:#""];
NSLog (#"Result: %#", resultString);
return resultString;
}
-(void)saveImage : (NSData *)Imagedata ImageString : (NSString *)imageString
{
NSArray* documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES);
NSString* documentDirectory = [documentDirectories objectAtIndex:0];
NSString* documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent:imageString];
if (![Imagedata writeToFile:documentDirectoryFilename atomically:NO])
{
NSLog((#"Failed to cache image data to disk"));
}
else
{
NSLog(#"the cachedImagedPath is %#",documentDirectoryFilename);
}
}
#end
/// call
[cell.ProductImage loadImageUsingUrlString:[[ArrProductList objectAtIndex:indexPath.row] valueForKey:#"product_image"] placeholder:[UIImage imageNamed:#"app_placeholder"]];

Resources