Async image loading from url inside a UITableView cell - image changes to wrong image while scrolling
IosObjective CCocoa TouchUitableviewIos Problem Overview
I've written two ways to async load pictures inside my UITableView cell. In both cases the image will load fine but when I'll scroll the table the images will change a few times until the scroll will end and the image will go back to the right image. I have no idea why this is happening.
#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString:
@"http://myurl.com/getMovies.php"]];
[self performSelectorOnMainThread:@selector(fetchedData:)
withObject:data waitUntilDone:YES];
});
}
-(void)fetchedData:(NSData *)data
{
NSError* error;
myJson = [NSJSONSerialization
JSONObjectWithData:data
options:kNilOptions
error:&error];
[_myTableView reloadData];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
// Return the number of rows in the section.
// Usually the number of items in your array (the one that holds your list)
NSLog(@"myJson count: %d",[myJson count]);
return [myJson count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell == nil) {
cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
}
dispatch_async(kBgQueue, ^{
NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];
dispatch_async(dispatch_get_main_queue(), ^{
cell.poster.image = [UIImage imageWithData:imgData];
});
});
return cell;
}
... ...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell == nil) {
cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
}
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]];
NSURLRequest* request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse * response,
NSData * data,
NSError * error) {
if (!error){
cell.poster.image = [UIImage imageWithData:data];
// do whatever you want with image
}
}];
return cell;
}
Ios Solutions
Solution 1 - Ios
Assuming you're looking for a quick tactical fix, what you need to do is make sure the cell image is initialized and also that the cell's row is still visible, e.g:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg", self.myJson[indexPath.row][@"movieId"]]];
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (data) {
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
if (updateCell)
updateCell.poster.image = image;
});
}
}
}];
[task resume];
return cell;
}
The above code addresses a few problems stemming from the fact that the cell is reused:
-
You're not initializing the cell image before initiating the background request (meaning that the last image for the dequeued cell will still be visible while the new image is downloading). Make sure to
nil
theimage
property of any image views or else you'll see the flickering of images. -
A more subtle issue is that on a really slow network, your asynchronous request might not finish before the cell scrolls off the screen. You can use the
UITableView
methodcellForRowAtIndexPath:
(not to be confused with the similarly namedUITableViewDataSource
methodtableView:cellForRowAtIndexPath:
) to see if the cell for that row is still visible. This method will returnnil
if the cell is not visible.
The issue is that the cell has scrolled off by the time your async method has completed, and, worse, the cell has been reused for another row of the table. By checking to see if the row is still visible, you'll ensure that you don't accidentally update the image with the image for a row that has since scrolled off the screen.
- Somewhat unrelated to the question at hand, I still felt compelled to update this to leverage modern conventions and API, notably:
-
Use
NSURLSession
rather than dispatching-[NSData contentsOfURL:]
to a background queue; -
Use
dequeueReusableCellWithIdentifier:forIndexPath:
rather thandequeueReusableCellWithIdentifier:
(but make sure to use cell prototype or register class or NIB for that identifier); and -
I used a class name that conforms to Cocoa naming conventions (i.e. start with the uppercase letter).
Even with these corrections, there are issues:
-
The above code is not caching the downloaded images. That means that if you scroll an image off screen and back on screen, the app may try to retrieve the image again. Perhaps you'll be lucky enough that your server response headers will permit the fairly transparent caching offered by
NSURLSession
andNSURLCache
, but if not, you'll be making unnecessary server requests and offering a much slower UX. -
We're not canceling requests for cells that scroll off screen. Thus, if you rapidly scroll to the 100th row, the image for that row could be backlogged behind requests for the previous 99 rows that aren't even visible anymore. You always want to make sure you prioritize requests for visible cells for the best UX.
The simplest fix that addresses these issues is to use a UIImageView
category, such as is provided with SDWebImage or AFNetworking. If you want, you can write your own code to deal with the above issues, but it's a lot of work, and the above UIImageView
categories have already done this for you.
Solution 2 - Ios
/* I have done it this way, and also tested it */
Step 1 = Register custom cell class (in case of prototype cell in table) or nib (in case of custom nib for custom cell) for table like this in viewDidLoad method:
[self.yourTableView registerClass:[CustomTableViewCell class] forCellReuseIdentifier:@"CustomCell"];
OR
[self.yourTableView registerNib:[UINib nibWithNibName:@"CustomTableViewCell" bundle:nil] forCellReuseIdentifier:@"CustomCell"];
Step 2 = Use UITableView's "dequeueReusableCellWithIdentifier: forIndexPath:" method like this (for this, you must register class or nib) :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomTableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"CustomCell" forIndexPath:indexPath];
cell.imageViewCustom.image = nil; // [UIImage imageNamed:@"default.png"];
cell.textLabelCustom.text = @"Hello";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// retrive image on global queue
UIImage * img = [UIImage imageWithData:[NSData dataWithContentsOfURL: [NSURL URLWithString:kImgLink]]];
dispatch_async(dispatch_get_main_queue(), ^{
CustomTableViewCell * cell = (CustomTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
// assign cell image on main thread
cell.imageViewCustom.image = img;
});
});
return cell;
}
Solution 3 - Ios
There are multiple frameworks that solve this problem. Just to name a few:
Swift:
- Nuke (mine)
- Kingfisher
- AlamofireImage
- HanekeSwift
Objective-C:
Solution 4 - Ios
Swift 3
I write my own light implementation for image loader with using NSCache. No cell image flickering!
ImageCacheLoader.swift
typealias ImageCacheLoaderCompletionHandler = ((UIImage) -> ())
class ImageCacheLoader {
var task: URLSessionDownloadTask!
var session: URLSession!
var cache: NSCache<NSString, UIImage>!
init() {
session = URLSession.shared
task = URLSessionDownloadTask()
self.cache = NSCache()
}
func obtainImageWithPath(imagePath: String, completionHandler: @escaping ImageCacheLoaderCompletionHandler) {
if let image = self.cache.object(forKey: imagePath as NSString) {
DispatchQueue.main.async {
completionHandler(image)
}
} else {
/* You need placeholder image in your assets,
if you want to display a placeholder to user */
let placeholder = #imageLiteral(resourceName: "placeholder")
DispatchQueue.main.async {
completionHandler(placeholder)
}
let url: URL! = URL(string: imagePath)
task = session.downloadTask(with: url, completionHandler: { (location, response, error) in
if let data = try? Data(contentsOf: url) {
let img: UIImage! = UIImage(data: data)
self.cache.setObject(img, forKey: imagePath as NSString)
DispatchQueue.main.async {
completionHandler(img)
}
}
})
task.resume()
}
}
}
Usage example
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier")
cell.title = "Cool title"
imageLoader.obtainImageWithPath(imagePath: viewModel.image) { (image) in
// Before assigning the image, check whether the current cell is visible
if let updateCell = tableView.cellForRow(at: indexPath) {
updateCell.imageView.image = image
}
}
return cell
}
Solution 5 - Ios
Here is the swift version (by using @Nitesh Borad objective C code) :-
if let img: UIImage = UIImage(data: previewImg[indexPath.row]) {
cell.cardPreview.image = img
} else {
// The image isn't cached, download the img data
// We should perform this in a background thread
let imgURL = NSURL(string: "webLink URL")
let request: NSURLRequest = NSURLRequest(URL: imgURL!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
let error = error
let data = data
if error == nil {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data!)
// Store the image in to our cache
self.previewImg[indexPath.row] = data!
// Update the cell
dispatch_async(dispatch_get_main_queue(), {
if let cell: YourTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? YourTableViewCell {
cell.cardPreview.image = image
}
})
} else {
cell.cardPreview.image = UIImage(named: "defaultImage")
}
})
task.resume()
}
Solution 6 - Ios
The best answer is not the correct way to do this :(. You actually bound indexPath with model, which is not always good. Imagine that some rows has been added during loading image. Now cell for given indexPath exists on screen, but the image is no longer correct! The situation is kinda unlikely and hard to replicate but it's possible.
It's better to use MVVM approach, bind cell with viewModel in controller and load image in viewModel (assigning ReactiveCocoa signal with switchToLatest method), then subscribe this signal and assign image to cell! ;)
You have to remember to not abuse MVVM. Views have to be dead simple! Whereas ViewModels should be reusable! It's why it's very important to bind View (UITableViewCell) and ViewModel in controller.
Solution 7 - Ios
In my case, it wasn't due to image caching (Used SDWebImage). It was because of custom cell's tag mismatch with indexPath.row.
On cellForRowAtIndexPath :
-
Assign an index value to your custom cell. For instance,
cell.tag = indexPath.row
-
On main thread, before assigning the image, check if the image belongs the corresponding cell by matching it with the tag.
dispatch_async(dispatch_get_main_queue(), ^{ if(cell.tag == indexPath.row) { UIImage *tmpImage = [[UIImage alloc] initWithData:imgData]; thumbnailImageView.image = tmpImage; }}); });
Solution 8 - Ios
Thank you "Rob"....I had same problem with UICollectionView and your answer help me to solved my problem. Here is my code :
if ([Dict valueForKey:@"ImageURL"] != [NSNull null])
{
cell.coverImageView.image = nil;
cell.coverImageView.imageURL=nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if ([Dict valueForKey:@"ImageURL"] != [NSNull null] )
{
dispatch_async(dispatch_get_main_queue(), ^{
myCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell)
{
cell.coverImageView.image = nil;
cell.coverImageView.imageURL=nil;
cell.coverImageView.imageURL=[NSURL URLWithString:[Dict valueForKey:@"ImageURL"]];
}
else
{
cell.coverImageView.image = nil;
cell.coverImageView.imageURL=nil;
}
});
}
});
}
else
{
cell.coverImageView.image=[UIImage imageNamed:@"default_cover.png"];
}
Solution 9 - Ios
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg", self.myJson[indexPath.row][@"movieId"]]];
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (data) {
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
if (updateCell)
updateCell.poster.image = image;
});
}
}
}];
[task resume];
return cell;
}
Solution 10 - Ios
I think you want to speed up your cell loading at the time of image loading for cell in the background. For that we have done the following steps:
-
Checking the file exists in the document directory or not.
-
If not then loading the image for the first time, and saving it to our phone document directory. If you don't want to save the image in the phone then you can load cell images directlyin the background.
-
Now the loading process:
Just include: #import "ManabImageOperations.h"
The code is like below for a cell:
NSString *imagestr=[NSString stringWithFormat:@"http://www.yourlink.com/%@",[dictn objectForKey:@"member_image"]];
NSString *docDir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0];
NSLog(@"Doc Dir: %@",docDir);
NSString *pngFilePath = [NSString stringWithFormat:@"%@/%@",docDir,[dictn objectForKey:@"member_image"]];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:pngFilePath];
if (fileExists)
{
[cell1.memberimage setImage:[UIImage imageWithContentsOfFile:pngFilePath] forState:UIControlStateNormal];
}
else
{
[ManabImageOperations processImageDataWithURLString:imagestr andBlock:^(NSData *imageData)
{
[cell1.memberimage setImage:[[UIImage alloc]initWithData: imageData] forState:UIControlStateNormal];
[imageData writeToFile:pngFilePath atomically:YES];
}];
}
ManabImageOperations.h:
#import <Foundation/Foundation.h>
@interface ManabImageOperations : NSObject
{
}
+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage;
@end
ManabImageOperations.m:
#import "ManabImageOperations.h"
#import <QuartzCore/QuartzCore.h>
@implementation ManabImageOperations
+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage
{
NSURL *url = [NSURL URLWithString:urlString];
dispatch_queue_t callerQueue = dispatch_get_main_queue();
dispatch_queue_t downloadQueue = dispatch_queue_create("com.myapp.processsmagequeue", NULL);
dispatch_async(downloadQueue, ^{
NSData * imageData = [NSData dataWithContentsOfURL:url];
dispatch_async(callerQueue, ^{
processImage(imageData);
});
});
// downloadQueue=nil;
dispatch_release(downloadQueue);
}
@end
Please check the answer and comment if there is any problem occurs....
Solution 11 - Ios
Simply change,
dispatch_async(kBgQueue, ^{
NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString: [NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];
dispatch_async(dispatch_get_main_queue(), ^{
cell.poster.image = [UIImage imageWithData:imgData];
});
});
Into
dispatch_async(kBgQueue, ^{
NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString: [NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];
cell.poster.image = [UIImage imageWithData:imgData];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
});
});
Solution 12 - Ios
You can just pass your URL,
NSURL *url = [NSURL URLWithString:@"http://www.myurl.com/1.png"];
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (data) {
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
yourimageview.image = image;
});
}
}
}];
[task resume];
Solution 13 - Ios
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
Static NSString *CellIdentifier = @"Cell";
QTStaffViewCell *cell = (QTStaffViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
If (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"QTStaffViewCell" owner:self options:nil];
cell = [nib objectAtIndex: 0];
}
StaffData = [self.staffArray objectAtIndex:indexPath.row];
NSString *title = StaffData.title;
NSString *fName = StaffData.firstname;
NSString *lName = StaffData.lastname;
UIFont *FedSanDemi = [UIFont fontWithName:@"Aller" size:18];
cell.drName.text = [NSString stringWithFormat:@"%@ %@ %@", title,fName,lName];
[cell.drName setFont:FedSanDemi];
UIFont *aller = [UIFont fontWithName:@"Aller" size:14];
cell.drJob.text = StaffData.job;
[cell.drJob setFont:aller];
if ([StaffData.title isEqualToString:@"Dr"])
{
cell.drJob.frame = CGRectMake(83, 26, 227, 40);
}
else
{
cell.drJob.frame = CGRectMake(90, 26, 227, 40);
}
if ([StaffData.staffPhoto isKindOfClass:[NSString class]])
{
NSURL *url = [NSURL URLWithString:StaffData.staffPhoto];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url
completionHandler:^(NSURL *location,NSURLResponse *response, NSError *error) {
NSData *imageData = [NSData dataWithContentsOfURL:location];
UIImage *image = [UIImage imageWithData:imageData];
dispatch_sync(dispatch_get_main_queue(),
^{
cell.imageView.image = image;
});
}];
[task resume];
}
return cell;}