ASIHTTPRequest simultaneous download of images on tableview takes time - iphone

I have used ASIHTTPRequest framework in my project to handle all network related tasks.
I have custom cell with thumbnail which is coming from web server and there are around 500 images so I have to reuse the cell to handle it. Due reusing of cell when we scroll through tableview we can see images of previous cells which will be replaced by new image.
If network connection is low its worse since it takes lot of time to download the image..so for that time you can see wrong image for particular because reusing cell so I need to find way so that this image replacement shouldn't be visible to user.
I am using ASIDownalod SharedCache method.
EDIT
NSString *reuseIdentifier = #"offerCell";
BRZOfferCell *offerCell = (BRZOfferCell*)[tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
if (offerCell==nil) {
offerCell = [[[BRZOfferCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier celltype:kDealCellTypeDealsList] autorelease];
}
[offerCell setImage:[UIImage imageNamed:IMAGE_NO_IMAGE]];
//---get the letter in the current section---
//NSString *alphabet = [mDealsIndex objectAtIndex:[indexPath section]];
//---get all deals beginning with the letter---
NSString* lSectionIndex = [NSString stringWithFormat:#"%i",[indexPath section]];
NSMutableArray *deals = [mIndexedOffersDic objectForKey:lSectionIndex];
if ([deals count]>0) {
//---extract the relevant deal from the deals array object---
Offer* lOffer = [deals objectAtIndex:indexPath.row];
[offerCell setOffer:lOffer];
offerCell.accessoryView = UITableViewCellAccessoryNone;
if (mTableView.dragging == NO && mTableView.decelerating == NO)
{
//Function : format image url to _thumb#2x.png and Initiate Image request download
//and set cache policy
[mListViewHelper InitImageRequest: lOffer.PromoImage indexPath: indexPath];
}
}
return offerCell;

As you said UITableView reuses cells in order to perform well, so you need to clear the cell before reuse it, or it's going to display the wrong data.
You also should use asynchronous calls, and some delegation to update cells.
I would actually take it a level higher and use NSOperationQueue, that allows you to set the maximum number of concurrent downloads, and canceling requests when leaving page.
What you might want to do is to create Data helpers
#protocol BookDataHelperDelegate <NSObject>
#required
- (void) bookDataHelperDidLoadImage:(BookDataHelper *)dataHelper;
#end
#interface BookDataHelper
#property (nonatomic, retian) UIImage *bookCover;
#property (nonatomic, retain) Book *book;
#property (nonatomic, assign) NSObject<BookDataHelperDelegate> *delegate;
- (void) fetchImageAsynchronouslyFromWebWithDelegate:(NSObject<BookDataHelperDelegate> *)delegate;
#end
This would be how you reload data on your table
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *SimpleTableIdentifier = #"SimpleTableIdentifier";
CustomCell *cell = [tableView
dequeueReusableCellWithIdentifier:SimpleTableIdentifier];
if (cell == nil)
{
cell = [[[CustomCell alloc]initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:SimpleTableIdentifier] autorelease];
}
BookDataHelper *dataHelper = [myArray objectAtIndex:indexPath.row];
if (!dataHelper.bookCover)
{
[cell.imageView setImage:nil];
[dataHelper fetchImageAsynchronouslyFromWebWithDelegate:self];
}
else
{
[cell.imageView setImage:dataHelper.bookCover];
}
cell.bookTitleLabel.text = dataHelper.book.title;
return cell;
}
- (void)bookDataHelperDidLoadImage:(BookDataHelper *)datahelper
{
[tableView reloadDate];
// here you would either reload the table completely
// Or you could reload specific cells
}

In your tableview cell delegate, when you get a reused or new, cell, clear the image before returning it. Update with the proper ownloaded image in an asynchronous callback. You might want to make sure the images are saved or retained somewhere else though if you don't want your app to keep redownloading them.

in ASIHTTPRequest framework its work on both type Synchronize and ASynchronize so firat tell me which one u use for get image data & also tell me that u send whole 500 image request at time or send as per your cell is loaded
or if you send 500 images request at a time than this on is not right as per the cell requirement send the request fro that cell image other wise its not feasible.

I have used ASIDownloadCache methods to solve my problem. Actually there are 2 solutions for this problem
Setting your own cache path instead of using SharedCache but i didn't went for this becuase I was already using sharedCache and found another efficient method which will avoid me changing my current implementation
In this approach I have used 2 methods from ASIDownloadCache methods(surprisingly ASIHTTPREquest website didn't mention these methods in their brief info)
2.1 First method - (BOOL)isCachedDataCurrentForRequest:(ASIHTTPRequest *)request
to verify if this particular image url is already cached or not if yes use 2nd method
2.2 - (NSData *)cachedResponseDataForURL:(NSURL *)url to get the cached image so that we can set the image in cellForRowAtIndexPath itself and you will not see image replacing issue due reusability of cell.
Here is the code :
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *reuseIdentifier = #"offerCell";
BRZOfferCell *offerCell = (BRZOfferCell*)[tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
if (offerCell==nil) {
offerCell = [[[BRZOfferCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier celltype:kDealCellTypeDealsList] autorelease];
}
[offerCell setImage:[UIImage imageNamed:IMAGE_NO_IMAGE]];
//---get the letter in the current section---
//NSString *alphabet = [mDealsIndex objectAtIndex:[indexPath section]];
//---get all deals beginning with the letter---
NSString* lSectionIndex = [NSString stringWithFormat:#"%i",[indexPath section]];
NSMutableArray *deals = [mIndexedOffersDic objectForKey:lSectionIndex];
if ([deals count]>0) {
//---extract the relevant deal from the deals array object---
Offer* lOffer = [deals objectAtIndex:indexPath.row];
[offerCell setOffer:lOffer];
offerCell.accessoryView = UITableViewCellAccessoryNone;
if ([mListViewHelper isCached:lOffer.PromoImage]) { // Is image available in Cache ?
// Image is available use image fomr cache directly
[offerCell setImage:[UIImage imageWithData:[mListViewHelper cacheDataWithNSURL:lOffer.PromoImage]]];
}
else{
//Function : Initiate Image request download and set cache policy
if (mTableView.dragging == NO && mTableView.decelerating == NO)
[mListViewHelper InitImageRequest: lOffer.PromoImage indexPath: indexPath];
}
}
return offerCell;
}

Related

iPhone - Asynchronous image only displays on scroll

I've been banging my head against the wall on this one and searched far and wide for a solution to no avail:
I have a large array of data pulled from the web and I'm using Loren Brichter's ABTableViewCell to make it run smoothly by drawing everything inside of the contentView of each cell to avoid UILabels and UIImageViews slowing scrolling down.
This works great for displaying text, but I run into a problem with images because of the time it takes to download them. I can't seem to find a way to force the contentView of each cell displayed to redraw itself once the corresponding image has been downloaded. I must point out I am not drawing labels and imageViews, but just the contentView in order to save memory.
Right now the table behaves like this:
Load: text displayed, no images
Scroll up or down: images finally
show up once the cells move off screen
A sample project is here
Code:
ABTableViewCell.h
#interface ABTableViewCell : UITableViewCell
{
UIView *contentView;
}
ABTableViewCell.m
- (void)setNeedsDisplay
{
[contentView setNeedsDisplay];
[super setNeedsDisplay];
}
- (void)drawContentView:(CGRect)r
{
// subclasses implement this
}
TableCellLayout.h
#import "ABTableViewCell.h"
#interface TableCellLayout : ABTableViewCell {
}
#property (nonatomic, copy) UIImage *cellImage;
#property (nonatomic, copy) NSString *cellName;
TableCellLayout.m
#import "TableCellLayout.h"
#implementation TableCellLayout
#synthesize cellImage, cellName;
- (void)setCellName:(NSString *)s
{
cellName = [s copy];
[self setNeedsDisplay];
}
- (void)setCellImage:(UIImage *)s
{
cellImage = [s copy];
[self setNeedsDisplay];
}
- (void)drawContentView:(CGRect)r
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextFillRect(context, r);
[cellImage drawAtPoint:p];
[cellName drawAtPoint:p withFont:cellFont];
}
TableViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
TableCellLayout *cell = (TableCellLayout *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil)
{
cell = [[TableCellLayout alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.cellName = [[array valueForKey:#"name"] objectAtIndex:indexPath.row];
cell.cellImage = [UIImage imageNamed:#"placeholder.png"]; // add a placeholder
NSString *imageURL = [[array valueForKey:#"imageLink"] objectAtIndex:indexPath.row];
NSURL *theURL = [NSURL URLWithString:imageURL];
if (asynchronousImageLoader == nil){
asynchronousImageLoader = [[AsynchronousImages alloc] init];
}
[asynchronousImageLoader loadImageFromURL:theURL];
cell.cellImage = asynchronousImageLoader.image;
return cell;
}
This is the final method the AsynchronousImageLoader calls once the image is prepared:
- (void)setupImage:(UIImage*)thumbnail {
self.image = thumbnail;
[self setNeedsLayout];
}
I just need the correct way to tell my visible cells to redraw themselves once the row's image has been downloaded. I imagine I should be putting something in that final method (setupImage)--but I can't seem to get it working the way it should. Thoughts? Many thanks!
Final edit: the solution
Right, so as suspected, the problem was that visible cells weren't being told to redraw and update to the downloaded image once the call was complete.
I used the help provided by the answers below to put together a solution that works well for my needs:
Added a callback in the final method that the asynchronous image downloader calls:
AsyncImageView.m
- (void)setupImage:(UIImage*)thumbnail {
self.cellImage = thumbnail;
[cell setNeedsDisplay];
}
Note: I also set a local placeholder image in the initialization of the image downloader class just to pretty things up a bit.
Then in my cellForRowAtIndexPath:
NSURL *theURL = [NSURL URLWithString:imageURL];
AsyncImageView *aSync = [[AsyncImageView alloc] initWithFrame:CGRectMake(0, 0, 20, cell.bounds.size.height)];
[aSync loadImageFromURL:theURL];
cell.cellImageView = aSync;
return cell;
There may have been one or two other tweaks, but those were the major problems here. Thanks again SO community!
Make sure that you are updating the cell image in the Main Thread. UI updates only appear if done there, which is why you only see the update when you touch & scroll.
if(cell) {
dispatch_async(dispatch_get_main_queue(), ^{
cell.cellImage = thumbnail;
[cell setNeedsDisplay];
});
}
[EDIT]
You need the cell to be the delegate of the image loader and own the async redraw mechanism.
......
AsynchronousImages *asynchronousImageLoader = [[AsynchronousImages alloc] init];
asynchronousImageLoader.delegate = cell;
[asynchronousImageLoader loadImageFromURL:theURL];
return cell;
}
And place the delegate call back code in the cell implementation.
- (void)setupImage:(UIImage*)thumbnail {
self.cellImage = thumbnail;
}
You can used the Apple TableView Lazy Loading. They have sample codes that download images asynchonously. See link below
Apple LazyTableImages
On your end in AsynchronousImages class you can add an attribute NSIndexPath and the delegate on AsynchronousImages should be change. See the code below
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
TableCellLayout *cell = (TableCellLayout *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil)
{
cell = [[TableCellLayout alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.cellName = [[array valueForKey:#"name"] objectAtIndex:indexPath.row];
cell.cellImage = [UIImage imageNamed:#"placeholder.png"]; // add a placeholder
NSString *imageURL = [[array valueForKey:#"imageLink"] objectAtIndex:indexPath.row];
NSURL *theURL = [NSURL URLWithString:imageURL];
AsynchronousImages *asynchronousImageLoader = [[AsynchronousImages alloc] init];
asynchronousImageLoader.indexPath = indexPath;
[asynchronousImageLoader loadImageFromURL:theURL];
return cell;
}
//Delegate should be
- (void)setupImage:(UIImage*)thumbnail index:(NSIndexPath*) indePath {
TableCellLayout *cell = (TableCellLayout *) [tableView cellForRowAtIndexPath:indexPath];
if(cell) {
cell.cellImage = thumbnail;
[cell setNeedsDisplay];
}
}
verify UIImage creation and setting of the UIImageView's image property happen on the main thread. there is no reason setting the image view's image should not invalidate its rect if visible.
also confirm that your loads are cancelled correctly, if you are reusing cells.
Use AsyncImageView in place of uiimageview
You can tell your tableView to reloadData... or a slightly more refined reload:
[self.tableView reloadRowsAtIndexPaths:self.tableView.indexPathsForVisibleRows withRowAnimation:UITableViewRowAnimationNone];
Edit to Add (after looking at OP's source):
I've looked through your project, and the problem is with your architecture. You're misusing AsyncImageView because you're only using it to asynchronously load your image - whereas it is designed to both load, and display the image. This is why it has no 'callback' function to let you know when the data has been retrieved.
You would be better off replacing your CellLayout's image property with a UIImageView property instead. (Note that UIImageView is more efficient at drawing than image drawAtPoint anyway).
So:
Change your CellLayout class to use an UIImageView property instead of UIImage
Change your cellForRowAtIndexPath to set the AsyncImageView as a property directly on your Cell.
If you want to support placeholders, that should be added to your AsyncImageView class - so that it knows what to display while downloading the content.
cellForRowAtIndexPath:
NSURL *theURL = [NSURL URLWithString:imageURL];
AsyncImageView *aSync = [[AsyncImageView alloc] initWithFrame:CGRectMake(0, 0, 20, cell.bounds.size.height)];
[aSync loadImageFromURL:theURL];
cell.cellImageView = aSync;
return cell;
CellLayout.h
#property (nonatomic, strong) UIImageView *cellImageView;
CellLayout.m
- (void)setCellImageView:(UIImageView *)s
{
[cellImageView removeFromSuperview];
cellImageView = s;
[self addSubview:cellImageView];
}

Caching images in UITableView

I'm loading pictures into a table view that correspond to the cell text, so each image is different. As the user scrolls down, iOS has to manually reload each image from the SSD, so scrolling is very choppy. How do I cache images or prevent the table view cells from needing to be recreated? Others have had their issues solved by using imageNamed: as iOS will automatically cache your images, but I am loading images from the documents directory, not the app bundle. What should I do? Thanks.
- (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.
return [issues count];
}
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
}
// Set up the cell...
NSDictionary *dic = [self.issues objectAtIndex:indexPath.row];
cell.text = [dic objectForKey:#"Date"];
cell.imageView.image = [UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#/issues/%#/cover.png", documentsDirectory, [dic objectForKey:#"Directory Name"]]];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 150;
}
That array of dictionaries is your model, so it would be a natural place. The tricky part is making your model mutable. You can either make your model mutable in the class, or jump through the hoops when you cache the image. I'd recommend the former, but coded it here to match your existing code...
- (UIImage *)imageForIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dic = [self.issues objectAtIndex:indexPath.row];
UIImage *image = [dic valueForKey:#"cached_image"];
if (!image) {
image = [UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#/issues/%#/cover.png", documentsDirectory, [dic objectForKey:#"Directory Name"]]];
NSMutableDictionary *updatedDictionary = [NSMutableDictionary dictionaryWithDictionary:dic];
[updatedDictionary setValue:image forKey:#"cached_image"];
NSMutableArray *updatedIssues = [NSMutableArray arrayWithArray:self.issues];
[updatedIssues replaceObjectAtIndex:indexPath.row withObject:[NSDictionary dictionaryWithDictionary:updatedDictionary]];
self.issues = [NSArray arrayWithArray:updatedIssues];
}
return image;
}
This will be choppy on the first scroll through the list, then smoother thereafter. If you'd like to have no first-time chop and incur a little latency before the view appears, you can walk your model ahead of time and force the load:
for (int i=0; i<self.issues.count; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
(void)[self imageForIndexPath:indexPath];
}
One last thing - Another solution is a mutable array to match your issues array. That may be just fine, but more often than not, I end up glad that I included some cached calculation with my model. If you find yourself using that issues array elsewhere, you'll be happy that you have the images all taken care of.
Along with caching you may also consider loading the images in background using Grand central dispatch. When the cell is loaded put a UIActivityIndicator then replace it with an image in a separate thread.
Also checkout this related answer for image stutter:
Non-lazy image loading in iOS

UITableView not refreshed

I have an app consisting of a TabBar with a few TabBarControllers. One Controller contains a very simple table, which is supposed to display the contents of a NSMutableDictionary. When you hit the appropriate button, the Dictionary is updated in a separate Controller and the view switches to the UITableViewController, displaying the newly updated table.
I can see the Dictionary being updated. But the TableView never reflects the changes. In fact, it seems to display the changes only the 1st time I enter that screen.
I have tried [self table.reloadData] and while it gets called, the changes aren't reflected to the UITableView.
Does anyone have any suggestions? I am happy to post code, but am unsure what to post.
Update: the table is updated and refreshed properly only the 1st time it is displayed. Subsequent displays simply show the original contents.
Background:
The tableview gets filled from a dictionary: appDelegate.currentFave. The tableview should get refreshed each time the ViewController is invoked by the TabBarController.
- (void)viewWillAppear:(BOOL)animated
{
NSLog(#"in viewWillAppear");
[super viewWillAppear:animated];
[self loadFavesFile];
[self.tableView reloadData];
}
// load the Favorites file from disk
- (void) loadFavesFile
{
// get location of file
NSString *path = [self getFavesFilePath];
// The Favorites .plist data is different from the Affirmations in that it will never be stored in the bundle. Instead,
// if it exists, then use it. If not, no problem.
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
// read Faves file and store it for later use...
NSMutableDictionary *tempDict = [NSMutableDictionary dictionaryWithContentsOfFile:path];
appDelegate.sharedData.dictFaves = tempDict;
// grab the latest quote. Append it to the list of existing favorites
NSString *key = [NSString stringWithFormat:#"%d", appDelegate.sharedData.dictFaves.count + 1];
NSString *newFave = [NSString stringWithFormat:#"%#", appDelegate.currentFave];
[appDelegate.sharedData.dictFaves setObject:newFave forKey:key];
} else {
NSLog(#"Favorites file doesn't exist");
appDelegate.sharedData.dictFaves = nil;
}
}
// this gets invoked the very first call. Only once per running of the App.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// reuse or create the cell
static NSString *cellID = #"cellId";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:cellID];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
}
// allow longer lines to wrap
cell.textLabel.numberOfLines = 0; // Multiline
cell.textLabel.lineBreakMode = UILineBreakModeWordWrap;
cell.textLabel.font = [UIFont fontWithName:#"Chalkduster" size:(16)];
cell.textLabel.textColor = [UIColor yellowColor];
// NOTE: for reasons unknown, I cannot set either the cell- or table- background color. So it must be done using the Label.
// set the text for the cell
NSString *row = [NSString stringWithFormat:#"%d", indexPath.row + 1];
cell.textLabel.text = [appDelegate.sharedData.dictFaves objectForKey:row];
return cell;
}
I found the problem. I was not properly initializing and assignng the TableView in my view controller. See below
- (void)viewDidLoad
{
[super viewDidLoad];
tableView = [[UITableView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame] style:UITableViewStylePlain];
tableView.dataSource = self;
tableView.delegate = self;
tableView.backgroundColor=[UIColor blackColor];
self.view = tableView;
}
Assuming the code you have put up is correct, you want to use [self.table reloadData]. You have the . in the wrong place.
I had this same problem yesterday, for me it turned out I had set the wrong file owner in interface builder and hadn't set up the data source and delegates for the table view properly.
Try going into interface builder and right-clicking on the file owner, this should show you if anything isn't connected up properly.
You should make sure that your Interface Builder connections are set up properly, but what this problem really sounds like is that you have your UITableViewCell setup code in cellForRowAtIndexPath: inside your if(cell == nil) statement. Which it shouldn't be. Let me explain. If you have a list of cells, and you want to set the titles to each cell to a string in an array called myArray, right now your (incorrect) code looks like this:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cellIdentifier"];
if (cell == nil) {
// No cell to reuse => create a new one
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:#"cellIdentifier"] autorelease];
[[cell textLabel] setText:[myArray objectAtIndex:[indexPath row]]];
}
return cell;
}
Can you see the problem with that logic? The cell will only get an updated title if no reusable cell can be found, which, in your case, sounds like the situation. Apple says that you should create a 'new' cell each time cellForRowAtIndexPath: is called, which means that you put all of your setup code outside of the if(cell == nil) check.
Continuing with this example, the proper code would look like this:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cellIdentifier"];
if (cell == nil) {
// No cell to reuse => create a new one
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:#"cellIdentifier"] autorelease];
}
[[cell textLabel] setText:[myArray objectAtIndex:[indexPath row]]];
return cell;
}
This way, the cell gets assigned the proper string whether or not a reusable cell is found and so calling reloadData will have the desired effect.

(iPad/iPhone) refresh a table cell while it is on screen

I have a table view with custom cells, each of which have images and some text which must be parsed from a webpage. I have and operation queue which gets the data from the page and calls the method (void)addLoadedImageAndExcerpt:(NSString *)imgExc in the tableviewcontroller after each page's data is loaded and stores the data in 2 arrays. I need each cell to refresh once the image and text that associated with it are loaded into these 2 arrays (named "articleExcerpts" and "imageDataObjects").
the method is as follows:
- (void)addLoadedImageAndExcerpt:(NSString *)imgExc {
NSArray *imgAndExcerpt = [imgExc componentsSeparatedByString:#"{|}"];
[articleExcerpts addObject:[imgAndExcerpt objectAtIndex:1]];
NSData * imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString: [imgAndExcerpt objectAtIndex:0]]];
[imageDataObjects addObject:imageData];
//count how many rows have been loaded so far.
loadedCount ++;
[self.table reloadData];//table is a UITableView
[imageData release];
}
the problem is, I can't get the cells to change while they are on screen. Once I scroll, they show the proper data, while they are on screen, I can't get them to change. I tried the methods outlined here and here, but they don't work. I tried calling tableView:cellForRowAtIndexPath: for the relevant row and modifying the variables, but that didn't solve anything because that method seems to create a new cell every time is is called, and doesn't get the existing ones (I'll post the code for that method further down).
Using [self.table reloadData] as I have it now doesn't seem do anything either, which really confuses me...
my tableView:cellForRowAtIndexPath: method (I bet the problem is here. I'm not convinced I creating my custom cells properly)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CustomizedCell";
CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomCell" owner:self options:nil];
for (id currentObject in topLevelObjects){
if ([currentObject isKindOfClass:[UITableViewCell class]]){
cell = (CustomCell *) currentObject;
break;
}
}
}
// Configure the cell...
//title
cell.titleString = [titles objectAtIndex:indexPath.row];
//date
cell.dateString = [dates objectAtIndex:indexPath.row];
//Photo. check if imageDataObjects array is complete up to the current row yet
if (loadedCount > indexPath.row) {
if ([imageDataObjects objectAtIndex:indexPath.row] != #"NA") {
cell.imageData = [imageDataObjects objectAtIndex:indexPath.row];
} else {
cell.imageData = NULL;
}
}
//Excerpt. check if loadedCount array is complete up to the current row yet
if (loadedCount > indexPath.row) {
cell.exerptString = [articleExcerpts objectAtIndex:indexPath.row];
}
return cell;
}
what am I missing?
I have had a similar problem before, and was able to get it working by including the lines
[table beginUpdates];
[table endUpdates];
at the end of the method where your data is received (so call them once you have the data to populate the cells).
Hope this works for you too!
Hmm, I think you're only supposed to interact with UI components in the main thread. NSOperationQueue stuff runs in another thread. Instead of calling
[self.table reloadData]
try
[self.table performSelectorOnMainThread:#selector(reloadData:) withObject:nil waitUntilFinished:NO]
As far as I understand, the image is being loaded, and then added to an array of images (imageDataObjects), and the row never updates.
First things first, are you sure that the method addLoadedImageAndExcrept is adding the images in order? Remember that NSArray objects are nil-terminated, and therefore, if you're adding an image for a row further, it won't appear if a previous image is nil. What happens if an image comes nil? The array will end abruptly. Use the "count" method on the array to check if this happens, add dummy objects, or swtich to a dictionary. This may not solve your current issue, but it's something to consider. (*)
Aside from that, if images are being loaded correctly, the only reason for your code to not work (in what I understand from the code), is that the table IBOutlet you added, is not connected.
*EDIT: I noticed that you're checking for #"NA" on the row (although I don't see where it's being set), so you probably already considered that

How do I cache something for a tableview?

I have a tableview with large images that fill the cells and the row heights are set based on the image size. Unfortunately, the table jerks badly when scrolling to the next cell.
I've been told that my tableview will scroll more smoothly if I cache the row heights and the images before they are loaded into the table.
All my data are stored in a plist.
How do I go about caching something?
What does the code look like and where does it go?
Thanks!
Here's my code for loading the images:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *detailTableViewCellIdentifier = #"Cell";
DetailTableViewCell *cell = (DetailTableViewCell *)
[tableView dequeueReusableCellWithIdentifier:detailTableViewCellIdentifier];
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"DetailTableViewCell" owner:self options:nil];
for(id currentObject in nib)
{
cell = (DetailTableViewCell *)currentObject;
}
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *Path = [[NSBundle mainBundle] bundlePath];
NSString *MainImagePath = [Path stringByAppendingPathComponent:([[appDelegate.sectionsDelegateDict objectAtIndex:indexPath.section] objectForKey:#"MainImage"])];
cell.mainImage.image = [UIImage imageWithContentsOfFile:MainImagePath];
return cell;
}
I'm also using the following for calculating the row height:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
AppDelegate *appDelegate = (DrillDownAppAppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *Path = [[NSBundle mainBundle] bundlePath];
NSString *MainImagePath = [Path stringByAppendingPathComponent:([[appDelegate.sectionsDelegateDict objectAtIndex:indexPath.section] objectForKey:#"MainImage"])];
UIImage *imageForHeight = [UIImage imageWithContentsOfFile:MainImagePath];
imageHeight = CGImageGetHeight(imageForHeight.CGImage);
return imageHeight;
}
EDIT: Here is the final code below.
#define PHOTO_TAG 1
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Photo";
UIImageView *photo;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
UIImage *theImage = [UIImage imageNamed:[[appDelegate.sectionsDelegateDict objectAtIndex:indexPath.section] objectForKey:#"MainImage"]];
imageHeight = CGImageGetHeight(theImage.CGImage);
imageWidth = CGImageGetWidth(theImage.CGImage);
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
photo = [[[UIImageView alloc] initWithFrame:CGRectMake(0, 0, imageWidth, imageHeight)] autorelease];
photo.tag = PHOTO_TAG;
[cell addSubview:photo];
} else {
photo = (UIImageView *) [cell viewWithTag:PHOTO_TAG];
[photo setFrame:CGRectMake(0, 0, imageWidth, imageHeight)];
}
photo.image = theImage;
return cell;
}
Caching is not a panacea for tableview performance. Caching is only valuable if there is something expensive to calculate, and you can avoid calculating it. If, on the other hand, you simply have too many views in your UITableViewCell, then caching will do nothing for you. If your row heights are all the same, then there's nothing to cache. If you use +[UIImage imageNamed:], then the system is already caching your images for you.
The most common first-order problem with UITableViewCells is putting too many subviews in them. How have you constructed your cell? Have you spent time studying the Table View Programming Guide, particularly A Closer Look at Table-View Cells? Understanding this document will save you much grief later.
EDIT: (Based on code above)
First, you're fetching a reusable cell, and then immediately throwing it away, reading a NIB and iterating over all the top level objects looking for a cell (one that looks almost exactly like the one you just threw away). Then you work out a string, which you use to open a file and read the contents. You do this every time UITableView wants a new cell, which is a lot. And you do it over and over again for the same rows.
Then, when UITableView wants to know the height, you read the image off of disk again. And you do that every time UITableView asks (and it may ask many times for the same row, though it does try to optimize this).
You should start by reading the UITableView Programming Guide I link above. That's hopefully going to help a lot. When you've done that, here are the things you should be thinking about:
You indicated that there is nothing but a single image view in this cell. Do you really need a NIB for that? If you do stick with a NIB (and there are reasons to use them in some case), then read the Programming Guide about how to implement a NIB-base cell. You should be using IBOutlet, not trying to iterate over the top-level objects.
+[UIImage imageNamed:] will automatically find files in your Resources directory without you having to work out the bundle's path. It will also cache those images for you automatically.
The point of -dequeueReusableCellWithIdentifier: is to fetch a cell that UITableView is no longer using and that you can reconfigure rather than you making a new one. You're calling it, but you immediately throw it away. You should check if it returned nil, and only load it out of the NIB if it did. Otherwise, you just need to change the image. Again, read the Programming Guide; it has many, many examples of this. Just make sure that you really try to understand what -dequeueReusableCellWithIdentifier: is doing, and don't treat it as just something you type at this point in the program.
If you do need to cache the heights, I did something like this (caching heights for a cell displaying an "article" object - article maybe one of several subclasses):
+ (CGFloat) heightForArticle: (Article*) article atWidth: (CGFloat) width {
static NSCache* heightCache = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
heightCache = [NSCache new];
});
NSAssert(heightCache, #"Height cache must exist");
NSString* key = #"unique"; //Create a unique key here
NSNumber* cachedValue = [heightCache objectForKey: key];
if( cachedValue )
return [cachedValue floatValue];
else {
CGFloat height = 40;//Perform presumably large height calculation here
[heightCache setObject: [NSNumber numberWithFloat: height] forKey: key];
return height;
}
}