Objective C - Multithreading issue - iphone

In my app, when user presses on the one of the tabs in UITabBar, it takes too much time to load view and show it to the user, so it may be confusing (it's because I load images from the web in the UITableView). So, I decided to use multithreading to show view before all the images finished loading.
I am using this code:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[SetsCustomCell alloc] initWithFrame:CGRectZero];
}
// getting url
NSURL* imgUrl = [[NSURL alloc]initWithString:[[mainArray objectAtIndex:
indexPath.row]valueForKey:#"imageURL"]];
//put this url and current cell in the dictionary
NSDictionary* params = [NSDictionary dictionaryWithObjectsAndKeys:
imgUrl,#"localUrl",cell,#"localCell", nil];
// multithreading time (calling loadImageWithParams method)
[self performSelectorInBackground:#selector(loadImageWithParams:)
withObject:params];
return cell;
}
-(void)loadImageWithParams:(NSDictionary*)params {
NSURL* url = [params objectForKey:#"localUrl"];
cell = [params objectForKey:#"localCell"];
UIImage* thumb = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
NSDictionary* backParams = [NSDictionary dictionaryWithObjectsAndKeys:
cell,#"localCell",thumb,#"thumb", nil];
[self performSelectorOnMainThread:#selector(setImage:)
withObject:backParams waitUntilDone:YES];
}
-(void)setImage:(NSDictionary*)params{
cell = [params objectForKey:#"localCell"];
UIImage* thumb = [params objectForKey:#"thumb"];
[cell.imageView setImage:thumb];
cell.imageView.hidden = NO;
[cell setNeedsLayout];
}
I have only two cells in the UITableView and the problem is that only second cell loads its image. The first cell is still empty. However, if I scroll the UITableView until the first cell is no longer visible, and it calls cellForRowAtIndexPath: again, first cell gets its image.
I have also tried to make multithreading with NSOperationQueue and GCD but have the same results.
It seems like I don't clearly understand how multithreading works but I will be veeeery grateful if someone will point me at my mistake.
Thanks!

The way I've done this in the past is to create an NSObject subclass to store the images. Then the datastore for the UITableView is an array of these objects instead of URL paths.
Then you can lazy load the image like this...
ObjectModel.h
#property (nonatomic, strong) UIImage *image;
ObjectModel.m
- (UIImage*)image
{
if (_image == nil) {
[self downloadImage];
return [UIImage imageNamed:#"placeholderImage"];
}
return _image;
}
- (void)downloadImage
{
//Put your async stuff here to download the image then reload the tableView when it's done.
}
By doing this you are storing the images on the datastore but also managing the download stuff in its own class and keeping your TVC clean.

It seems like you're overcomplicating this -- I don't really understand what you're doing with the params dictionary. Why not present the controller, then start your download in viewDidAppear. When the results come back, populate your array with them, and call reloadData. Do the cells have anything in them besides the images that you can load right away?

Typical practice for this is to implement a LazyUIImage class which downloads/loads the image in the background. While the image is downloading, display something like a UIActivityIndicator. When the image arrives in the LazyUIImage, send out a notification and stop the activity indicator (and remove it from the view perhaps). For every view controller that contains a LazyUIImage, handle that notification, and e.g., perform [myTable reloadData]. In LazyUIImage, write a method called getImage which returns nil while the image is loading, and after the image has loaded, return the image.
Edit: actually, if you implement the LazyUIImage as a LazyUIImageView then you don't need to reload anything in containing view controllers: in the LazyUIImageView place a top level background view, and then switch out the UIActivityIndicator child view for the actual UIImageView once the image finishes downloading.

You need to do Lazy loading stuff.. Search for lazy loading using NSOperationQueue.

Related

Custom UITableViewCell as a delegate - UI elements are getting unexpectedly reused

I've created a custom UITableViewCell and each cell is passed a custom subclass of AVPlayer object from the UITableViewController. On each cell I have a play button, pause button and loading indicator.
When I play the audio, elements work as required, and change when player state has changed e.g. when playing, pause button appears, play button disappears. When I play audio on a second cell, the first cell knows this, resets it button state, and the second cell does it's business.
So this functionality works perfectly, the only problem is because the UITableViewCells get reused, when I scroll down to cells below, I start seeing the pause button appear on them. This is because they are the same cells as the ones above (reused) and because my cells are delegates for my custom subclass of AVPlayer, the audio player is sending messages to a cell which isn't the correct one.
What can I do to make each UITableViewCell a separate delegate object for my AVPlayer?
You have to remove the elements from the cells upon reuse:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
} else {
/* prepare for reuse */
[cell.playButton removeFromSuperview];
/* or */
[[cell viewWithTag:10] removeFromSuperview];
}
I had the same problem with images I'm loading from a JSON blob. I used GCD and saved my images to an NSDictionary paired with the key assigned to each cell.
- (UIImage *)imageForRowAtIndexPath:(NSIndexPath *)indexPath
{
// get the dictionary for the indexPath
NSDictionary *tweet = [tweets objectAtIndex:[indexPath row]];
// get the user dictionary for the indexPath
NSDictionary *user = [tweet objectForKey:#"posts"];
//get the image URL
NSURL *url = [NSURL URLWithString:[[[[[tweet objectForKey:#"photos"] objectAtIndex:0] objectForKey:#"alt_sizes"] objectAtIndex:3] objectForKey:#"url"]];
// get the user's id and check for a cached image first
NSString *userID = [user objectForKey:#"id"];
UIImage *image = [self.images objectForKey:userID];
if(!image)
{
// if we didn't find an image, create a placeholder image and
// put it in the "cache". Start the download of the actual image
image = [UIImage imageNamed:#"Placeholder.png"];
[self.images setValue:image forKey:userID];
//get the string version of the URL for the image
//NSString *url = [user objectForKey:#"profile_image_url"];
// create the queue if it doesn't exist
if (!queue) {
queue = dispatch_queue_create("image_queue", NULL);
}
//dispatch_async to get the image data
dispatch_async(queue, ^{
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *anImage = [UIImage imageWithData:data];
[self.images setValue:anImage forKey:userID];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
//dispatch_async on the main queue to update the UI
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = anImage;
});
});
}
// return the image, it could be the placeholder, or an image from the cache
return image;
}
I solved the problem by making the UITableViewController the delegate for the audio player. Then saving the "currently playing" cell's indexPath to a #property in the UITableViewController.
Then in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath I check to see if the indexPath is the same as the "currently playing" cell's indexPath, if so set the "audio playing" button arrangent, if not then set the default button arrangement.
This works better in distinguishing cells as you have a unique identifier indexPath to compare them with.

ASIHTTPRequest simultaneous download of images on tableview takes time

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;
}

Table View with Images, slow load and scroll

I tried impletmenting about 30 tutorials today and just cant get anything to work.
My problem is that i load my information via a JSON file, add the data to a NSMutableArray, then use a table to display it all. It works fine when i dont have the images, but when i do its loads very slow and scrolls very sticky. I sorta understand after todays findings that its reloading the images every scroll which is why its slow.
Can someone please break it down and make it easier for me to solve this problem?
Alex
Take a look at Apple's LazyTableImages example. Basically it comes down to
a) reusing your table cells
b) only loading images that are currently visible
You kinda left your problem wide open b/c you weren't specific enough. Performance issues can be related to a bunch of things.
Here are a few performance things with tableview cells & images
• Load images on a background thread.
• Reuse cells - don't allocate any more than you need on screen
static NSString *CellIdentifier = #"Cell";
CellClass *cell = (CellClass*)[tv dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) cell = [[[CellClass alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
• Only draw images that are the same size of the cell (ie. if a cell is 44 px high, keep UIimages at 44px). If images are bigger, you might have to process the images after downloading them from the internet.
• Don't use uiimageview in your cell. instead create a custom cell (ie. subclass) and draw the image in your drawRect: function.
You should use asynchronous image retrieval provided by UIImageView categories found in AFNetworking or SDWebImage. These categories:
are incredibly easy to use (rather than using the UIImageView method setImage, instead use one of the categories' setImageWithURL methods);
provide asynchronous image retrieval;
cache the downloaded images with NSCache, to make sure you don't have to retrieve images that you just downloaded;
ensure that your UI cannot get backlogged downloading images for cells that have scrolled off screen; and
leverage operation queues to constrain the degree of concurrency (rather than using GCD global queues that can result in timeout failures).
I have a class that I call RemoteImageHandler. Here is the .h file:
#import <UIKit/UIKit.h>
#interface RemoteImageHandler : NSObject
- (void)imageForUrl:(NSURL*)url callback:(void(^)(UIImage *image))callback;
+ (RemoteImageHandler *)shared;
#end
And the .m file:
#import "RemoteImageHandler.h"
#interface RemoteImageHandler ()
#property (nonatomic, strong) NSMutableDictionary *imageDictionary;
#end
#implementation RemoteImageHandler
- (void)imageForUrl:(NSURL*)url callback:(void(^)(UIImage *image))callback {
if (!!self.imageDictionary[url]) {
callback(self.imageDictionary[url]);
} else {
dispatch_async(dispatch_get_global_queue(0,0), ^{
NSData * data = [[NSData alloc] initWithContentsOfURL:url];
if (data == nil)
callback(nil);
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [UIImage imageWithData:data];
self.imageDictionary[url] = image;
callback(image);
});
});
}
}
+ (TQRemoteImageHandler *)shared {
static TQRemoteImageHandler *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [[self alloc] init];
});
return shared;
}
#end
In my table view, whenever I want an image from a remote location (let's say this is in cellForRowAtIndexPath, I use this:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier forIndexPath:indexPath];
[[RemoteImageHandler shared] imageForUrl:someURLCorrespondingToTheImageYouWant callback:^(UIImage *image) {
cell.imageView.image = image;
}];
return cell;
}

How to set the table view cell accessory view to retain a previously initialized UIImageView?

Let's say I have a property in my view controller, defined as follows:
#property (nonatomic, retain) UIImageView *checkmarkOffAccessoryView;
I #synthesize this in the implementation, release it in -dealloc and initialize it in -viewDidLoad as follows:
self.checkmarkOffAccessoryView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"checkmarkOff.png"]] autorelease];
So far so good.
When I use it in my table view delegate as an accessory view for multiple cells, two things happen:
Only one cell's accessory view shows the image
The application UI freezes.
The app doesn't crash, as near as I can tell, the UI simply becomes unresponsive. This is both in the simulator and on the device.
Here is how I use the initialized property with my cell:
- (UITableViewCell *) tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// initialize or dequeue cell...
if (condition)
cell.accessoryView = self.checkmarkOffAccessoryView;
else
cell.accessoryView = nil;
}
With the aforementioned code, only one cell shows the accessory view and the UI freezes.
If I initialize the UIImageView instance directly in the delegate method I get all condition-satisfying cells showing the accessory view and I do not experience the UI freeze:
- (UITableViewCell *) tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// initialize or dequeue cell...
if (condition)
cell.accessoryView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"checkmarkOff.png"]] autorelease];
else
cell.accessoryView = nil;
}
My goal is to initialize as few objects as possible and reuse one UIImageView. I'm curious why the first chunk of code is problematic and what I could do to fix this.
It seems like the cell's accessoryView property should just increment the retain count of self.checkmarkOffAccessoryView but it appears I am missing some detail.
What have I overlooked? Thanks for your advice.
EDIT
I think that:
self.checkmarkOffAccessoryView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"checkmarkOff.png"]] autorelease];
is the same as:
UIImageView *uncheckedView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"checkmarkOff.png"]];
self.checkmarkOffAccessoryView = uncheckedView;
[uncheckedView release];
Either way, I experience the same freeze symptom.
You cannot add the same view multiple times. The UI handler will go bonkers. To make sure of this, I tried doing what you said above and I got the same issue. The UI freezes up, the image only appears for one of the cells.
The best thing you can do is to store your image as a UIImage allocated, and to have a helper function which returns a new UIImageView per cell.
Using your current method (without a stored UIImage) you might do:
-(UIImageView *) makeCheckmarkOffAccessoryView
{
return [[[UIImageView alloc] initWithImage:
[UIImage imageNamed:#"checkmarkOff.png"]] autorelease];
}
And then do
cell.accessoryView = [self makeCheckmarkOffAccessoryView];
As you may be aware, UIImages on the other hand may be used any number of times. a UIImageView doesn't take up a lot of space, so you can easily have a bunch of those without worrying.
To expand on the one place only deal, imagine that you add a UIView to two places at the same time.
What will [ob removeFromSuperview] do for this object? Will it remove the view from both places? From one of them only? Which value will be returned when you request [ob superview]? Clearly the UI is not made to handle what you're asking for.
Try it without the autorelease in the initializer. I suspect you're over-releasing.
By the way, your console probably is showing a BAD_ACCESS error when it freezes. If you turn on NSZombieEnabled, my guess is you'll see it's making a call to a deallocated UIImage.
maybe this will help
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"ShoppingListCell";
HSShoppingListCell *cell = (HSShoppingListCell *)[aTableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:#"ShoppingListCell"
owner:self
options:nil];
cell = shoppingListCell;
}
ShoppingListItem *theItem = nil;
theItem = [self.fetchedResultsController objectAtIndexPath:indexPath];
UIImage *selected = [UIImage imageNamed:#"listBullet_checked.png"];
UIImage *notSelected = [UIImage imageNamed:#"listBullet.png"];
cell.imageView.image = ([theItem.checkedOff boolValue] ? selected : notSelected);
cell.shoppingListLabel.text = theItem.productName;
[cell.shoppingListLabel setFont:[UIFont fontWithName:#"Marker Felt" size:26.0]];
return cell;
}
- (void)toggleCellImage:(NSIndexPath *)indexPath
{
ShoppingListItem *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
item.checkedOff = ([item.checkedOff boolValue] ? [NSNumber numberWithBool:NO] : [NSNumber numberWithBool:YES]);
[HSCoreDataUtilities saveContext:item.managedObjectContext];
[self.tableView reloadData];
}
#pragma mark -
#pragma mark Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self toggleCellImage:indexPath];
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}
Reducing your case to the bare essentials (I was going to suggest to put two 'thin' UIView objects around the UIImageView...), I found that it is most probably impossible.
Create 2 empty UIView objects in IB, hook them up to bareView1 and bareView2. Then
UIImageView *imageView = [[UIImageView alloc]
initWithImage:[UIImage imageNamed:#"test.png"]];
[bareView1 addSubview:imageView]; // it shows either here ...
[bareView2 addSubview:imageView]; // ... or here
You can never get the image on sceen more than once like this. As a rule of thumb, I think the first object in line which does not inherit from UIView can be used multiple times, i.e. the UIImage. Like Kalle stated, a UIView can only have one parent in the view hierarchy.
Postponing the second addSubview only makes the UIImageView jump from bareView1 to bareView2.
The freeze happens maybe because the event handling gets mixed up: the accessory can be interactive, how would you know which one was tapped if they are one and the same object? So the code assumes objects are unique, and you manage to violate that assumption.

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;
}
}