Loading Images into a UITableView Asynchronously iOS 5 - iphone

I'm struggling to get my tableView scrolling smoothly when loading images into the cell. At the moment I'm doing:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (self.newsFeedArray != nil) {
NewsFeedCustomCell *cell = [tableView dequeueReusableCellWithIdentifier:#"NewsCell"];
self.singleStory = [self.newsFeedArray objectAtIndex:indexPath.row];
NSString *imageURL = [NSString stringWithFormat:#"%#", [self.singleStory valueForKey:#"image_primary"]];
NSURL *imageLink = [NSURL URLWithString:imageURL];
cell.newsFeedImage.image = [UIImage imageWithData: [NSData dataWithContentsOfURL:imageLink]];
cell.newsTitleLabel.text = [NSString stringWithFormat:#"%#", [self.singleStory valueForKey:#"title"]];
return cell;
} else {
NewsFeedCustomCell *cell = [tableView dequeueReusableCellWithIdentifier:#"LoadingCell"];
return cell;
}
}
I've tried loading the images in on a background thread, but it takes the object out of the array and means it only loads in the first image 10 times.
I've tried implementing LazyTableImages but got confused and lost with what I actually needed.
Advice/help/work arounds would be greatly appreciated!

Related

TableView images from url

I found a strange problem. I have a JSON web service that returns me:
title
image (link)
featured (bool)
descriprion
I have next code to populate my cells:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"PasakaCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleTableIdentifier];
}
Pasaka *retrievedPasaka = [allPasakas objectAtIndex:indexPath.row];
cell.textLabel.text = retrievedPasaka.title;
NSString *ImageURL = retrievedPasaka.image;
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:ImageURL]];
cell.imageView.image = [UIImage imageWithData:imageData];
cell.accessoryType =UITableViewCellAccessoryDisclosureIndicator;
return cell;
}
Only 4 images are being displayed:
Link that is being displayed:
#".../img/desa.png"
Link that is not displayed:
#".../img/38 papagaiļi.png"
Maybe the problem is in the links? With such symbols as: ļ,ā,ī etc.
And if so, how to resolve this? Because the image names on the server have those symbols in names...
I see that second link contains whitespace. This is incorrect.
Change ImageURL initialisation to NSString *ImageURL = [retrievedPasaka.image stringByAddingPercentEscapesUsingEncoding:NSUTD8StringEncoding];
If it does not help take a look into URL syntax.

What is the proper way to use NSCache with dispatch_async in a reusable table cell?

I have been looking for a clear cut way to do this and have not found anywhere that will give an example and explain it very well. I hope you can help me out.
Here is my code that I am using:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"NewsCell";
NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
NewsItem *item = [newsItemsArray objectAtIndex:indexPath.row];
cell.newsTitle.text = item.title;
NSCache *cache = [_cachedImages objectAtIndex:indexPath.row];
[cache setName:#"image"];
[cache setCountLimit:50];
UIImage *currentImage = [cache objectForKey:#"image"];
if (currentImage) {
NSLog(#"Cached Image Found");
cell.imageView.image = currentImage;
}else {
NSLog(#"No Cached Image");
cell.newsImage.image = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^(void)
{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:item.image]];
dispatch_async(dispatch_get_main_queue(), ^(void)
{
cell.newsImage.image = [UIImage imageWithData:imageData];
[cache setValue:[UIImage imageWithData:imageData] forKey:#"image"];
NSLog(#"Record String = %#",[cache objectForKey:#"image"]);
});
});
}
return cell;
}
The cache returns nil for me.
Nitin answered the question about how to use a cache quite well. The thing is, both the original question and Nitin's answer suffer from a problem that you're using GCD, which (a) doesn't provide control over the number of concurrent requests; and (b) the dispatched blocks are not cancelable. Furthermore, you're using dataWithContentsOfURL, which is not cancelable.
See WWDC 2012 video Asynchronous Design Patterns with Blocks, GCD, and XPC, section 7, "Separate control and data flow", about 48 min into the video for a discussion of why this is problematic, namely if the user quickly scrolls down the list to the 100th item, all of those other 99 requests will be queued up. You can, in extreme cases, use up all of the available worker threads. And iOS only allows five concurrent network requests anyway, so there's no point in using up all of those threads (and if some dispatched blocks start requests that can't start because there are more than five going, some of your network requests will start failing).
So, in addition to your current approach of performing network requests asynchronously and using a cache, you should:
Use operation queue, which allows you to (a) constrain the number of concurrent requests; and (b) opens up the ability to cancel the operation;
Use a cancelable NSURLSession, too. You can do this yourself or use a library like AFNetworking or SDWebImage.
When a cell is reused, cancel any pending requests (if any) for the previous cell.
This can be done, and we can show you how to do it properly, but it's a lot of code. The best approach is to use one of the many UIImageView categories, which do cacheing, but also handles all of these other concerns. The UIImageView category of SDWebImage is pretty good. And it greatly simplifies your code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"NewsCell"; // BTW, stay with your standard single cellIdentifier
NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier indexPath:indexPath];
NewsItem *item = newsItemsArray[indexPath.row];
cell.newsTitle.text = item.title;
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:item.image]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
return cell;
}
Might be you did some wrong you setting same Key for each image of NSCache
[cache setValue:[UIImage imageWithData:imageData] forKey:#"image"];
Use this Instead of above set ForKey as a Imagepath item.image and use setObject instead of setVlaue:-
[self.imageCache setObject:image forKey:item.image];
try with this Code example:-
in .h Class:-
#property (nonatomic, strong) NSCache *imageCache;
in .m class:-
- (void)viewDidLoad
{
[super viewDidLoad];
self.imageCache = [[NSCache alloc] init];
// the rest of your viewDidLoad
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"cell";
NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
NewsItem *item = [newsItemsArray objectAtIndex:indexPath.row];
cell.newsTitle.text = item.title;
UIImage *cachedImage = [self.imageCache objectForKey:item.image];;
if (cachedImage)
{
cell.imageView.image = cachedImage;
}
else
{
cell.imageView.image = [UIImage imageNamed:#"blankthumbnail.png"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:item.image]];
UIImage *image = nil;
if (imageData)
image = [UIImage imageWithData:imageData];
if (image)
{
[self.imageCache setObject:image forKey:item.image];
}
dispatch_async(dispatch_get_main_queue(), ^{
UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (updateCell)
cell.imageView.image = [UIImage imageWithData:imageData];
NSLog(#"Record String = %#",[cache objectForKey:#"image"]);
});
});
}
return cell;
}

iOS Table View stuck in loop while trying to cache images

I am using async to get my images from an xml. Parser is working correctly and I can output the URLs. In the async I am trying to cache the images to a mutable dictionary. I am stuck in a loop and the images will not output at all any more. Here is my code that I am stuck on.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"Got Here");
static NSString *CellIdentifier = #"NewsCell";
NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSLog(#"Got Here 2");
// Configure the cell...
NewsItem *item = [newsItemsArray objectAtIndex:indexPath.row];
cell.newsTitle.text = item.title;
NSLog(#"Got Here 3");
NSMutableDictionary *record = [_records objectAtIndex:[indexPath row]];
if ([record valueForKey:#"actualImage"]) {
NSLog(#"Record Found");
[cell.newsImage setImage:[record valueForKey:#"actualImage"]];
} else {
NSLog(#"Record Not Found");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,(unsigned long)NULL), ^(void)
{
NSLog(item.image);
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:item.image]];
dispatch_async(dispatch_get_main_queue(), ^(void){
[record setValue:[UIImage imageWithData:imageData] forKey:#"actualImage"];
[cell.newsImage setImage:[record valueForKey:#"actualImage"]];
if (tableView) {
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
});
});
}
return cell;
}
Thanks in advance for you help.
Create a UITableViewCell subclass. When you need to load an image in it, create an NSOperation subclass which does the actual network connection. (see http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/ for an example). Alternatively, use a 3rd party package which handles this, there are millions of them. My current favorite is MKNetworkKit. Store a reference to your operation. In the cell's prepareForReuse, cancel the connection if it hasn't completed. This will prevent your cell getting the wrong image when scrolling if the first request completes after the second (happens more often then you'd think).
First of all replace
[record setValue:[UIImage imageWithData:imageData] forKey:#"actualImage"];
with
[record setObject:[UIImage imageWithData:imageData] forKey:#"actualImage"];
and your code should work. Since the dictionary is always empty it always goes to else block.
Print NSMutableDictionary and you will realize this.
This is not the way caching is done. It's not only about memory but also about coding practices.
Please use a clean approach like NSCache or SDWebImage
SDWebImage takes care of caching as well.
Use NSOperation to make request rather than GCD. to avoid nested blocks and concerrency issues.
Here is how a clean code looks like
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"cell";
NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NewsItem *item = [newsItemsArray objectAtIndex:indexPath.row];
cell.newsTitle.text = item.title;
// category from library
[cell.imageView setImageWithURL:newsItem.imageURL];
if ([item needsRefresh] )
{
cell.newsTitle.text = item.title;
NewsOperation *operation = [[NewsOperation alloc] initForNewsItem:newItem completion:^(NewsItem *newsItem, NSError *error) {
if (newsItem)
{
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
}];
[[self newsItemQueue] addOperation:operation];
}
return cell;
}
You may try the following code. You should also research your problem and try to find other existing answers and explanations. This is a very frequent question, countless times answered on SO ;)
For example: loading images in table using blocks
The code tries to fix the most obvious issues, but there is no guarantee that it works in all edge cases. You need to debug and test.
The main problem with the original code is, that it tries to set the cell's newsImage property within the completion block. When that gets executed, the captured cell is likely not be associated to the same row anymore. Remember: cells will be reused!
Thus, in the completion block, the changed code re-evaluates the corresponding cell, from the indexPath which has been captured at the start of the block:
NewsCell* cell2 = [tableView cellForRowAtIndexPath:indexPath]; // cell equals nil, if not visible
The returned cell may be not visible, in which case the table view returns nil.
Basically, the same is applied to the record variable. Just for different reasons: the record variable is a variable with automatic scope, that is it gets released when the method returns. Capturing it in the block is probably not a good idea.
Thus, it needs to be re-evaluted in the block. Thats basically a short hand for [self.records objectAtIndex:[indexPath row]];
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"NewsCell";
NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
NewsItem *item = [newsItemsArray objectAtIndex:indexPath.row];
cell.newsTitle.text = item.title;
NSMutableDictionary *record = [_records objectAtIndex:[indexPath row]];
if ([record valueForKey:#"actualImage"]) {
NSLog(#"Record Found");
[cell.newsImage setImage:[record valueForKey:#"actualImage"]];
} else {
NSLog(#"Record Not Found");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,(unsigned long)NULL), ^(void)
{
NSLog(item.image);
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:item.image]];
dispatch_async(dispatch_get_main_queue(), ^(void){
NSMutableDictionary *record2 = [_records objectAtIndex:[indexPath row]];
[record2 setValue:[UIImage imageWithData:imageData]
forKey:#"actualImage"];
NewsCell* cell2 = [tableView cellForRowAtIndexPath:indexPath]; // cell equals nil, if not visible
if (cell2) {
[cell2.newsImage setImage:[record2 valueForKey:#"actualImage"]];
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
});
});
}
return cell;
}

Table view mixes up images

I use SDWebImage to load and display async images in TableView. But sometimes when I scroll up and down fast, it mixes up all images and display it in other rows. This is my cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
// Configure the cell.
MWFeedItem *item = [itemsToDisplay objectAtIndex:indexPath.row];
if (item) {
// Parse out Image URL for cell
if (cell.imageView.image == nil) {
NSError *error = NULL;
NSRegularExpression *regexImage = [NSRegularExpression regularExpressionWithPattern:#"(<img\\s[\\s\\S]*?src\\s*?=\\s*?['\"](.*?)['\"][\\s\\S]*?>)+?"
options:NSRegularExpressionCaseInsensitive
error:&error];
[regexImage enumerateMatchesInString:item.content
options:0
range:NSMakeRange(0, [item.content length])
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSString *src = [item.content substringWithRange:[result rangeAtIndex:2]];
NSLog(#"img src: %#", src);
[cell.imageView setImageWithURL:[NSURL URLWithString:src]];
}];}
if (cell.imageView.image == nil) {
cell.imageView.image = [UIImage imageNamed:#"video.png"];
}
return cell;
}
I don't know what is wrong, but I think its because I parse the image in the cell and it is not fast enough so it starts again and again. Can you please show me how to fix that
The high-level answer is, I believe, that when you scroll you are reusing the cells before you complete putting an image into them. That's what it means when I see this in my code, anyway.
I don't use SDWebImage and I don't know exactly what setImageWithURL variants are, but the github webpage has a how-to-use that says you can give it a completion block to execute when the image fetch is done (succeeds or fails).
So you need to check, when you finally have the image but before you put it in the UITableViewCell, that the cell is still assigned to the same indexPath as when you started to get the image. Since setImageWithURL seems to set the image always, you will have to put have a temporary UIImageView rather than directly in the cell. The docs I looked at had a method call with both placeHolderImage: and completed: Using that you'd do something like (code not compiler checked):
// before you go off to get the image, save the indexPath of the cell
NSIndexPath *originalIndexPath = indexPath;
UIImageView *tempImageView;
[tempImageView setImageWithURL:[NSURL URLWithString : [NSURL URLWithString:src]]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
// completion block
// You probably want to check that it really succeeded, but if it did
// Now you have an image in the image parameter and probably in tempImageView.
// Check to see if the cell is still at the original indexPath
// If so, put the image in.
// If not, the row was scrolled out of sight and the cell has been reused
// so just drop the image on the floor -- it is no longer useful
NSIndexPath *currentIndexPath = [self.tableview indexPathForCell:cell]
if ([currentIndexPath isEqual originalIndexPath]) {
cell.imageView.image = image;
// or perhaps: cell.imageView.image = tempImageView.image;
}
}];
Change cell identifier from
static NSString *CellIdentifier = #"Cell";
to
NSString *CellIdentifier = [NSString stringWithFormat:#"Cell%i",indexPath.row];

UITableView only shows data from JSON when I scroll up and down

UITableView only shows data from JSON array when I scroll up and down. When I load the table view it shows cells but they are blank and as I scroll up and down it then starts showing the data.
How do I show the cells with data without scrolling?
- (void)requestFinished:(ASIHTTPRequest *)request
{
if (request.responseStatusCode == 400) {
NSLog( #"Code already used");
} else if (request.responseStatusCode == 403) {
NSLog( #"Code already used");
} else if (request.responseStatusCode == 200) {
NSLog(#"%#",[request responseString]);
NSString *response = [request responseString];
const char *convert = [response UTF8String];
NSString *responseString = [NSString stringWithUTF8String:convert];
responseArray = [responseString JSONValue];
Nrows = [responseArray count];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
NSDictionary *dict = [responseArray objectAtIndex:indexPath.row];
cell.detailTextLabel.text = [dict objectForKey:#"allfeeds"];
cell.textLabel.adjustsFontSizeToFitWidth = YES;
cell.textLabel.font = [UIFont systemFontOfSize:12];
cell.textLabel.minimumFontSize = 10;
cell.textLabel.numberOfLines = 4;
cell.textLabel.lineBreakMode = UILineBreakModeWordWrap;
cell.textLabel.text = [dict objectForKey:#"allfeeds2"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}
When you finish your request and you set the array, make sure to call [self.tableView reloadData];
For performance and animation purposes, table views don't reload their data when the source changes.
Instead of starting the request asynchronously (non-blocking)
[request startAsynchronously];
Try doing it synchronously (blocking)
[request startSynchronously];
I had a similar issue and got my answer at the Apple's Developer Forums a while ago, this solved my problem:
From what you've described, I'd guess the problem is more likely to be in cellForRowAtIndexPath or wherever you do the actual configuration of a cell than in the code you've posted. A problem like yours can happen when dequeueReusableCellWithIdentifier gives you back an existing cell, but you don't reset its existing state completely.
The full question and answers can be found here (requires login): https://devforums.apple.com/message/502483#502483
These problems are usually caused by the UITableView cache, that dequeues his available cached UITableViewCell with the same CellIdentifier (in your case - #"Cell").
Try creating a cell identifier for each and everyone of your table's cells. You can create this variance using the NSIndexPath param in the following way :
NSString* cellIdentifier = [NSString stringWithFormat:#"Cell%d",indexPath.row];
That of course, under the assumption you're not changing the content of your cells after you load the data.