In my app i have to fetch images from a json webservice and display them in a table view. While scrolling downwards all the images come correct order but while i revert backwards all the images get over each other. I am using [ tableview reloaddata] then too its happening.
This happened to me in my app, where I have a leaderboards table. It is definitely because the tables are being reused.
The easiest way to fix this is simply setting the cell's image to nil first, then downloading and displaying the new image.
There are definitely other ways to do this, but I can definitely vouch for this one, as I use it in my own app, and it works great!!
Here's how I solve the problem.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString * cellIdentifier = #"Cell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
// Make sure you set the image for the cell to nil first
[cell.imageView setImage:nil];
// Then load your new image using the url and display it
dispatch_queue_t backgroundQueue = dispatch_queue_create("com.example.imagedownloadqueue", NULL);
dispatch_async(backgroundQueue, ^(void){
NSUrl * url = [NSUrl UrlWithString:#"www.example.com"]; // Url of image to download
UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfUrl:url]];
if (image)
{
dispatch_async(dispatch_get_main_queue(), ^(void) {
[cell.imageView setImage:image]; // Display image after it is downloaded
});
}
});
return cell;
}
Related
im making a tableview loaded with some NSArrays, the cell contains two labels and a background image view loaded with a URL image. The problem is that the scrolling of the tableview is slow, its like freeze or something.... i think that is because of the Imageview but what can i do???
heres some of my code so you can see:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _Title.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
int row = [indexPath row];
cell.TitleLabel.text = _Title[row];
cell.DescriptionLabel.text = _content[row];
cell.CellImageView.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:_ImageUrl[row]]]];
cell.CellImageView.contentMode = UIViewContentModeTop;
cell.CellImageView.contentMode = UIContentSizeCategoryMedium;
cell.backgroundColor = [UIColor clearColor];
return cell;
}
For extra info this tableview presents a Detailviewcontroller with this same info.
Load the image asynchronously(load it as the images come in). Look into great classes such as SDWebImage https://github.com/rs/SDWebImage
You are blocking the main thread with the following line of code:
cell.CellImageView.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:_ImageUrl[row]]]];
My suggestion would be to use AFNetworking and replace the code with the following:
[cell.cellImageView setImageWithURL:[NSURL URLWithString:#"http://example.com/image.png"]];
Also, your pointers should start with a lower case letter. For instance, CellImageView should be cellImageView.
You are calling [NSData dataWithContentsOfURL:[NSURL URLWithString:_ImageUrl[row]]] from cellForRowAtIndexPath , which is not good idea. Because it'll go to the server every time a UITableViewCell is loaded.
You must use Asynchronous image loading & cache. These libraries might help you : (My personal favourite is SDWebImage)
1) https://github.com/rs/SDWebImage
2) https://github.com/nicklockwood/AsyncImageView
And more, you can refer to the sample code by Apple about LazyTableImages - https://developer.apple.com/library/ios/samplecode/LazyTableImages/Introduction/Intro.html
UPDATE:
For SDWebImage follow this guide. It's very good.
http://iosmadesimple.blogspot.in/2013/04/lazy-image-loading.html
To expand on #vborra's answer - the reason why your tableview is slow is that (in your code) the entire image MUST have finished downloading before it displays.
This is because dataWithContentsOfURL: is a synchronous method. You need to use asynchronous APIs to download images in the background and when they've finished downloading, display them on the screen.
Here is a code snippet from the github page adapted for your example. Make sure you add the folder SDWebImage and it's contents from https://github.com/rs/SDWebImage to your project.
#import <SDWebImage/UIImageView+WebCache.h>
...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
int row = [indexPath row];
cell.TitleLabel.text = _Title[row];
cell.DescriptionLabel.text = _content[row];
[cell.CellImageView setImageWithURL:[NSURL URLWithString:_ImageUrl[row]]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
cell.CellImageView.contentMode = UIViewContentModeTop;
cell.CellImageView.contentMode = UIContentSizeCategoryMedium;
cell.backgroundColor = [UIColor clearColor];
return cell;
}
Note, if you're downloading images of all different sizes, you may end up with resizing issues which will also decrease your performance. The SDWebImage page has a link to an article to help you with this problem if you come across it. http://www.wrichards.com/blog/2011/11/sdwebimage-fixed-width-cell-images/
You may also experience a performance bump when using transparency and tableviews, but it depends on the rest of your code.
AFNetworking is an excellent tool to use, but might be overkill if you're not using networking anywhere else in your app.
load your image like this
//get a dispatch queue
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//this will start the image loading in bg
dispatch_async(concurrentQueue, ^{
NSData *imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:mapUrlStr]];
//this will set the image when loading is finished
dispatch_async(dispatch_get_main_queue(), ^{
cell.CellImageView.image = [UIImage imageWithData:imageData]
});
});
Hi my problem is that when I scroll TableView the image will appear in a wrong cell, after a few seconds the correct image appears.
here is my code
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] ;
}
[cell setOpaque:NO];
[cell setBackgroundColor: [UIColor clearColor]];
PlaceData *data = [tableData objectAtIndex:indexPath.row];
UILabel *nameLabel = (UILabel *)[cell viewWithTag:100];
UILabel *sciNameLabel = (UILabel *)[cell viewWithTag:200];
UIImageView *thumbnailImageView = (UIImageView *)[cell viewWithTag:300];
nameLabel.text = data.name;
sciNameLabel.text = data.scientific_name;
// get a dispatch queue
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// this will start the image loading in bg
dispatch_async(concurrentQueue, ^{
NSURL *urlToPicture = [NSURL URLWithString:[NSString stringWithFormat:#"%#", data.thumbnail]];
NSData *imgData = [NSData dataWithContentsOfURL:urlToPicture options:0 error:nil];
// This will set the image when loading is finished
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
thumbnailImageView.image = tmpImage;
//dispatch_release(concurrentQueue);
});
});
return cell;
}
please help me
You can try adding following code to your cellForRowAtIndexPath -
1) Assign an index value to your custom cell. For instance,
cell.tag = indexPath.row
2) 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;
}});
});
You are reusing old cells, which is a good thing. However, you are not initializing the image in the cell's image view, which is not such a good thing. What you're describing happens because an old cell, with an image that was already loaded for that cell, is used for the new cell. You are then loading that cell's image in the background (which, again, is good) but it takes a few moments for that image to fully load. In the meantime, the image that was already loaded on the old cell, is displayed (and that's the reason you're seeing a wrong image in that cell, for a few moments).
The solution? add either
thumbnailImageView.image = nil
or
thumbnailImageView.image = someDefaultImageWhileYourRealOneLoads
right before dispatch_queue_t concurrentQueue ....
That way, you won't see the old (irrelevant) image while the real one loads.
I hope this helps. Good luck.
As becauase your ImageView is being loaded in an async dispatch call which is NOT on the main thread and is being called in some other thread so there is a delay in fetching the data from the URL and then converting it to an UIImage. THis process takes a bit of time as you know but you are scrolling the tableview in a faster rate. And as you know cellForRowAtIndexPath reuses any cell that is out of the window so the cell that is being reused might NOT fetched the imagedata that it WAS TO RETRIEVE previously when it was in the Window. Thus it loads the wrong data and then again when async is fired for that specific cell the cell loads that image but there comes the delay.
To overcome this feature as Chronch pointed it out u can leave the imageview as nil OR you can use AFNetworking's own UIImageView catagory which has a superb class to load imageview images quite elegantly
I'll leave u a link to it AFNetworking
I would do all my data binding at - tableView:willDisplayCell:forRowAtIndexPath: only because at cellForRowAtIndexPath your cell hasn't been drawn yet.
Another solution you can use is AFNetworking like someone else mentioned before me.
Swift 3.0
DispatchQueue.main.async(execute: {() -> Void in
if cell.tag == indexPath.row {
var tmpImage = UIImage(data: imgData)
thumbnailImageView.image = tmpImage
}
})
cell.thumbnailimages.image=nil
cell.thumbnailimages.setImageWith(imageurl!)
I think these two lines solve your problem.
I am having a problem viewing my tableview when i get the data of my cells from a server. If i do not use photos there is no breaks in the scrolling, but i want to use the images also. Can anyone knows how can i resolve this? I am getting the data from a plist in my server.
Here is the code of the image that makes the scrolling breaks (i am using custom style cell)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSURL *URL = [[NSURL alloc] initWithString:[[self.content objectAtIndex:indexPath.row] valueForKey:#"imageName"]];
NSData *URLData = [[NSData alloc] initWithContentsOfURL:URL];
UIImage *img = [[UIImage alloc] initWithData:URLData];
UIImageView *imgView = (UIImageView *)[cell viewWithTag:100];
imgView.image = img;
....
If you mean that the scrolling stops and starts, this might be because if the images are loaded from a server (which might take a noticeable amount of time to do), executing on the main thread causes freezing.
If this is the case, the fix is to fetch the images in another thread. Fortunately iOS has a fairly easy to use multithreading system called Grand Central Dispatch.
Here's some example code:
dispatch_queue_t q = dispatch_queue_create("FetchImage", NULL);
dispatch_async(q, ^{
/* Fetch the image from the server... */
dispatch_async(dispatch_get_main_queue(), ^{
/* This is the main thread again, where we set the tableView's image to
be what we just fetched. */
});
});
I am building my cellViews like this:
- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* cellIdentifier=#"cell";
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if(cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
}
UIImageView cellView = [[UIImageView alloc] initWithFrame:rectCellFrame];
NSError* error=nil;
NSData* imageData = [NSData dataWithContentsOfURL:imageArray[indexPath.row] options:NSDataReadingUncached error:&error];
UIImage* theImage= [UIImage ImageWithData:imageData];
[cellView setImage:theImage];
[cell addSubView:cellView];
.
.
.
.
[cell addSubView:moreViews];
}
Since the loading time (even when the images are cached) is very slow, I need to make this concurrent. But I would like to still be using my code with UIViews/UIImageViews.
Is there a way for me to show a placeholder and when relevant, ie cellView is finished building from all subviews, update the image instead of the placeholder?
Sure. You can set up all the heavy slow code in a asynchronous task. It's often down when images need to be downloaded. I'm sure it's covered in at least 1 of the WWDC videos on Table Views, but I've no clue which one or how old it would be by now.
- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// place holder image for the moment
[cellView setImage:placeHolderImage];
// run code to get the real image in asynchronous task
dispatch_async(self.contextQueue, ^{
UIImage *realImage = [thingy imageFromTimeConsumingTask];
// update cell on main thread (you need to do all UI stuff on main thread)
dispatch_async(dispatch_get_main_queue(), ^{
[cellView setImage:realImage];
});
});
}
I have looked around on SO and the web and have not found a specific answer.
Quite simply, I have a table loading images info from Flickr. I want to display a thumbnail of the image on the left side of each cell.
In order to do this without blocking the main (UI) thread, I am using blocks:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"top50places";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
//getting the selected row image
NSDictionary* currentImageDictionary=[self.topfifty objectAtIndex:indexPath.row];//topFifty is an array of image dictionaries
//creating the download queue
dispatch_queue_t downloadQueue=dispatch_queue_create("thumbnailImage", NULL);
dispatch_async(downloadQueue, ^{
UIImage *downloadedThumbImage=[self getImage:currentImageDictionary] ;
//Need to go back to the main thread since this is UI related
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = downloadedThumbImage ;
});
});
dispatch_release(downloadQueue);
return cell;
}
Now this is not going to work. Because returning the cell will likely happens before executing the block. But at the same time, I cannot return the cell within the main queue block because the block does not accept a return argument.
I want to avoid creating a UITableViewCell subclass.
Any simple answer using blocks?
Thanks
KMB
The problem with doing cell.imageView.image = downloadedThumbImage; is that this cell may now be reused and used for another row.
Instead you need to update the current cell in the specific indexPath (the indexPath will be the same)
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.imageView.image = image;
Or sometimes what I do is to update the model and then reload the cell at the specific indexPath:
myModel.image = downloadedThumbImage;
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"top50places";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
//getting the selected row image
NSDictionary* currentImageDictionary=[self.topfifty objectAtIndex:indexPath.row];//topFifty is an array of image dictionaries
UIImage *currentImage = [currentImageDictionary objectForKey:#"image"];
if (currentImage) {
// we already fetched the image, so we just set it to the cell's image view
cell.imageView.image = currentImage;
}
else {
// we don't have the image, so we need to fetch it from the server
// In the meantime, we can set some place holder image
UIImage *palceholderImage = [UIImage imageNamed:#"placeholder.png"];
cell.imageView.image = palceholderImage;
// set the placeholder as the current image to your model, so you won't
// download the image multiple times (can happen if you reload this cell while
// download is in progress)
[currentImageDictionary setObject:palceholderImage forKey:#"image"];
// then download the image
// creating the download queue
dispatch_queue_t downloadQueue=dispatch_queue_create("thumbnailImage", NULL);
dispatch_async(downloadQueue, ^{
UIImage *downloadedThumbImage=[self getImage:currentImageDictionary] ;
//Need to go back to the main thread since this is UI related
dispatch_async(dispatch_get_main_queue(), ^{
// store the downloaded image in your model
[currentImageDictionary setObject:image forKey:#"image"];
// update UI
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.imageView.image = image;
});
});
dispatch_release(downloadQueue);
}
return cell;
}