Caching images in UITableView - iphone

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

Related

dequeueReusableCellWithIdentifier forIndexPath

I have a custom/sub-classed UITableViewCell using NSLayoutConstraints. The left most UIImageView may or may not be populated with an image, and if not, the objects to right of this field will naturally shift to the left. Works well within the custom UITableViewCell, however I'll be populating the content with my custom UITableViewController. Unfortunately, following code will populate every row cell.dispatchedView with an image -- though I've trapped to not populate rows with anything.
This has something to do with the dequeueReusableCellWithIdentifier. Ideas?
static NSString *CellIdentifier = #"CellIdentifier";
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[NXGActiveUnitsCell class] forCellReuseIdentifier:CellIdentifier];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
NSDictionary *item = [self.model.dataSource objectAtIndex:indexPath.row];
NXGActiveUnitsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if([[[item objectForKey:#"cfs_no"] clean] length] > 0) {
cell.dispatchedView.image = [UIImage imageNamed:#"p.png"];
NSLog(#">>>%#",[item objectForKey:#"cfs_no"] );
} else {
NSLog(#">>> i got nothing so I should not touch this row...");
}
cell.departmentIconView.image = [UIImage imageNamed:#"f.png"];
cell.unitLabel.text = [item valueForKey:#"unit_id"];
return cell;
}
I don't know if this already solves your problem, but you should
definitely set
cell.dispatchedView.image = nil;
in the else-case, because cells are reused.

UITableView scrolling crashes app

When i scroll UITableView it crashes the app. Here is code.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *tempDict = [albums objectAtIndex:indexPath.row];
static NSString *CellIdentifier = #"ImageCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
} else {
AsyncImageView *oldImage = (AsyncImageView *)[cell.contentView viewWithTag:999];
[oldImage removeFromSuperview];
}
I set the breakpoints and it stops at line
NSDictionary *tempDict = [albums objectAtIndex:indexPath.row];
What am i doing wrong?
Thanks in advance.
I am not sure, first of all please check the total number of rows and your array count. They must be same. If still it crashes then put this line
NSDictionary *tempDict = [[albums objectAtIndex:indexPath.row] copy];
Check size of albums array. May be it is smaller then indexPath.row or it was released somewhere.
Make sure you are not reading out of bounds of your NSdictionary.
Check your return value in the tableView:numberOfRowsInSection: dataSource method.
The most likely reason for a crash on that line is that your table view has more rows than you have items in your albums array. Normally, in a setup like this, you'd have exactly the same number of rows in your table, and your implementation of –tableView:numberOfRowsInSection: would look like this
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// assuming that there is only one section
return [albums count];
}
I solved this problem. Actually in the $cellForRow ,I was trying to get information from server, I did not apply lazy loading there. Because of that while scrolling the tableView, app was crashing.
Thank you to all for your help.

Problem with IndexPath.row

i have 60 rows in an table view.i have an array named as "BundleImagesArray " with 60 bundle images names.So i am retrieving the images from bundle and creating thumbnail to it.
whenever binding each cell at first time ,i am storing the Thumbnail images in to an array.because of enabling the fast scrolling after bind each cell.i am utilizing the images from the array(without creating the thumbnail again).however the imageCollection array(which will store thumbnail images) is disorder some times
the Index path.row is coming as 1,2.....33,34,50,51..etc
its not an sequential order.so i am getting trouble with my imageCollection array which is used to store and retrieve images according to the index path.may i know what is the reason for that.can any one provide me a good solution for this.is there any way to get the indexpath.row as sequential order?
my cellForRowAtIndexPath code is:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"IndexPath Row%d",indexPath.row);
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
if ([cell.contentView subviews])
{
for (UIView *subview in [cell.contentView subviews])
{
[subview removeFromSuperview];
}
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell setAccessoryType:UITableViewCellAccessoryNone];
Class *temp = [BundleImagesArray objectAtIndex:indexPath.row];
UIImageView *importMediaSaveImage=[[[UIImageView alloc] init] autorelease];
importMediaSaveImage.frame=CGRectMake(0, 0, 200,135 );
importMediaSaveImage.tag=indexPath.row+1;
[cell.contentView addSubview:importMediaSaveImage];
UILabel *sceneLabel=[[[UILabel alloc] initWithFrame:CGRectMake(220,0,200,135)] autorelease];
sceneLabel.font = [UIFont boldSystemFontOfSize:16.0];
sceneLabel.textColor=[UIColor blackColor];
[cell.contentView addSubview:sceneLabel];
//for fast scrolling
if([imageCollectionArrays count] >indexPath.row){
importMediaSaveImage.image =[imageCollectionArrays ObjectAtIndex:indexPath,row];
}
else {
//for start activity indicator
[NSThread detachNewThreadSelector:#selector(showallstartActivityBundle) toTarget:self withObject:nil];
NSData *datas = [self photolibImageThumbNailData:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:temp.fileName ofType:#"png" inDirectory:#"Images"]]];
importMediaSaveImage.image =[UIImage imageWithData:datas];
[imageCollectionArrays addObject:importMediaSaveImage.image];
//to stop activity indicator
[NSThread detachNewThreadSelector:#selector(showallstopActivityBundle) toTarget:self withObject:nil];
}
sceneLabel.text = temp.sceneLabel;
temp = nil;
return cell;
}
Getting the tableview to call you in a particular order is not the right solution. You need to be able to provide any cell it needs in any order.
I think the problem is that you are using an array and trying to access indexes that don't exist yet. You could use an NSMutableDictionary for your imageCollectionArrays (instead of NSArray) and store your data there, keyed by row (or NSIndexPath). They you can add or retrieve them in any order.
An NSArray is perfectly fine for UITableView rows. The rows are ordered, and an array is also ordered, so no problem there.
The problem in your code seems to be that you are using two arrays, one immutable and one mutable. You are adding objects to the mutable array, and so it is not exactly predictable where they will be added (because it is not predictable that the rows are going to be needed in order).
I recommend filling your NSMutableArray first with [NSNull null] objects, and then inserting the image at a specific point in the array:
// during initialization
for (int i=0; i<numberOfImages; i++) {
[imageCollectionArrays addObject:[NSNull null]];
}
// in cellForRowAtIndexPath:
[imageCollectionArrays replaceObjectAtIndex:indexPath.row
withObject:importMediaSaveImage.image];
Try that, see if it works.

iPhone: How to add rows in tableview after tabelview already loaded?

I have implemented "Add to Favourites" functionality for my iPhone application. It works fine except adding cells into my Favourite Table View during runtime. For example, I have given following methods.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
tableView.hidden = YES;
warningLabel.hidden = YES;
// Load any change in favourites
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [defaults objectForKey:kFavouriteItemsKey];
self.favourites = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if([self.favourites count] > 0)
tableView.hidden = NO;
else
warningLabel.hidden = NO;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.favourites count]; // Favorites is a dictionary contains required data
}
- (UITableViewCell *)tableView:(UITableView *)tView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
cell.textLabel.text = [NSString stringWithFormat:#"index: %d",indexPath.row];
return cell;
}
This code works fine and display rows correctly for the first time only! After tableview is loaded and if I add new item(s) in favorites or delete any item(s), it doesn't make any difference to my tableview! I want to display exactly what is available in Favourites dictionary. It seems CellForRowAtIndexPath doesn't get invoked when ViewAppear again. Is there any method for TableView that I can use to achieve my requirements?
I think you've missed to call [tableView reloadData];
The very easy way is to just call [tableView reloadData] whenever you make any changes.
There is also a better (faster for large tables, and possibly animated; more elegant), but much more complicated way which I won't go into unless you decide the reloadData way isn't sufficient for you.

Group Table View - loading data

Looking for help on how to properly load the data for a Grouped Table View. I am using following code to load the data which I think should prepare an array "details" for "cellForRowAtIndexPath". However when I run the program I get the same data for all groups and it happens to be the last row in my data (which is in an NSArray called "Users"). Obviously I am doing something wrong but not sure what... please help.
// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSRange aRange;
aRange.length = 9; aRange.location = 16;
NSString *users = [self.Users objectAtIndex: section];
NSString *subtitle = [NSString stringWithString:[users substringWithRange:aRange]];
details = [[NSArray arrayWithObjects:subtitle, subtitle, subtitle, nil] retain];
return [details count];
}
The data loading routine is as follows:
- (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];
}
// Configure the cell.
UIImage *cellImage = [UIImage imageNamed:#"world.png"];
cell.imageView.image = cellImage;
cell.textLabel.text = [details objectAtIndex:indexPath.row];
return cell;
}
From what you've posted, details array is being reset every time the table views asks its data sources for the number of rows in a section. details is last set when -tableView:numberOfRowsInSection: is last called i.e. for the last section. Once set, the same array provides the data for cell.textLabel.text for all the sections. So you are getting incorrect data.
It would be appropriate to calculate and store all the details arrays in a different array, say detailsArray.
The data can then be accessed them like
[[detailsArray objectAtIndex:indexPath.section] objectAtIndex:indexPath.row]
Another problem with your code is that every time numberOfRowsInSection is called, data is being leaked as details is being reset without the earlier object being released.