For an iPhone app I am developing I need to assign custom uitableviewcell images based on the integers of arbitrary items in an array. I noticed while doing some testing that indexPath of row only returns indices for the view shown, so basically I need to know how to take an array, of arbitrary size, let's say right now it has 10 items, and those 10 items are in a table, each item a cell, I need each item to have an index like item 1 is index 0, item 2 is index 1 and so on. So my code would read, if the index == 0, then display this image in the cell, if the index != 0 then display another. I tried that but like I said if I scrolled to the bottom of my tableview it would reassign whichever table item is at the top of the view to 0, so as I scrolled the images kept changing. So basically i need help assigning the images based on an index in my array rather than on the index of the table.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:MyIdentifier] autorelease];
}
cell.detailTextLabel.lineBreakMode = UILineBreakModeWordWrap;
cell.detailTextLabel.numberOfLines = 1;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
NSString *title =[[stories objectAtIndex: indexPath.row] objectForKey: #"summary"];
NSString *title2 =[[stories objectAtIndex: indexPath.row] objectForKey: #"title"];
NSString *dayOfMonthString = [title substringWithRange: NSMakeRange(2, 2)];
int dateChooser = [dayOfMonthString intValue];
NSString *monthString = [title substringWithRange: NSMakeRange(0, 2)];
int monthChooser = [monthString intValue];
cell.textLabel.text =title2;
cell.imageView.image =
[UIImage imageNamed: [NSString stringWithFormat: #"cal%d.png", dateChooser]];
if (monthChooser == 1 && (INDEX COMPARISON CODE???){
UIImageView *myImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Jan1.png"]];
[cell setBackgroundView:myImageView];
}
Your problem is not getting the right index for the right cell. The row property of the indexPath do correspond to the index of the cell in the whole list of cells, not the index of the visible cells only, so exactly as you expected initially.
I bet your problem is that you don't use the reuse mechanism of UITableViewCells correctly
.
When you scroll in your TableView, UITableViewCells that are not on screen anymore are "recycled" and reused to display new cells onscreen, in order to avoid too much allocations and useless initializations that would else slow down the scrolling of your tableView.
The correct code pattern for returning a cell is the following:
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
// Try to retrieve a previously created cell that is not used anymore
// That's the "recycling" part, we try to reuse a cell instead of creating a new one if possible
UITableViewCell* cell = [tableView dequeueCellWithIdentifier:#"myIdentifier"];
if (cell == nil)
{
// We failed to recycle a previously created cell, so we have no choice to allocate a new one
cell = [[[UITableViewCell alloc] initWithStyle:... reuseIdentifier:#"myIdentifier"] autorelase];
// As we just created the cell, we configure everything that will be common to every cell
// and that won't change even if the cell is reused later when you scroll
// e.g. text color, text font, accessoryType, ...
cell.textLabel.textColor = [UIColor blueColor];
...
}
// Now we got here either with a brand new cell that have just been created…
// … or after reusing an old cell that were not onscreen and has been recycled
// So THIS IS THE PLACE to configure everything that is SPECIFIC to each cell
// namely cell text, especially
cell.textLabel.text = [yourDataArray objectAtIndex: indexPath.row];
return cell;
}
Related
I have a uitableview with a list of people in it. some records have an image and some records do not. If i scroll down through the list it appears correct but if i scroll back up then an image of another person starts to show on other people's cell row where an image should not be. Here is my code for cellForRowAtIndexPath
// START CELL LABELLING FOR TABLE VIEW LIST //
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(!cell){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
Person *person = [arrayOfPersons objectAtIndex:indexPath.row];
NSString *personPhoto = person.personPhoto;
NSString* imgPath = [docPath stringByAppendingPathComponent:
[NSString stringWithFormat:#"%#", personPhoto] ];
if ([[NSFileManager defaultManager] fileExistsAtPath:imgPath]){
NSLog(#"FILE EXISTS");
imageView = [[UIImageView alloc] initWithFrame:CGRectMake(240, 0, 67, 67)];
imageView.image = [UIImage imageWithContentsOfFile:imgPath];
imageView.contentMode = UIViewContentModeScaleAspectFit;
[cell.contentView addSubview:imageView];
}else{
NSLog(#"Does Not Exist");
}
cell.textLabel.text = person.personName;
return cell;
imageView = nil;
personPhoto = #"";
imgPath = #"";
}
// END CELL LABELLING FOR TABLE VIEW LIST //
The reason this is happening is because table cells get re-used.
When you use [tableView dequeueReusableCellWithIdentifier:CellIdentifier], you get back a cell that has already been shown in your table view (for a different index path). You may have already added an image view to this cell for the Person at this previous index path, and nowhere do you remove this image.
Therefore, when the current Person doesn't have a photo, the previous image will remain visible in the cell.
I would recommend creating your own UITableViewCell subclass and adding a UIImageView to it, so that you can easily get a reference back to the image view (or you could use a view tag, if you prefer).
Either way, you need to remove the image view/set the image to nil when the person does not have a photo.
I'll try to implement a TableView with 2 different kinds of cells, the first one is a higher Cell with a fullwidth image inside, all other cells got a little thumb image on the left side.
My problem it the performance of the TableView. If I scroll a list of maybe 20 items it jerks a bit. I've red something the performance and I'll hope the code is not so bad:
"Caching of images is not necessary to raise the performance" Is this right?
Does I reuse the cells in the right way.
Is that the normal way to use 2 different kinds of Cells?
Here is the important Part:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0 && indexPath.row == 0) {
return 160;
}
return 60;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellName = [[NSString alloc] init];
// [...]
if(indexPath.section == 0 && indexPath.row == 0){
cellName = #"FirstMainCell";
CellTemplateFirstNews *cell = [tableView dequeueReusableCellWithIdentifier:cellName];
if(cell == nil){
cell = [[[NSBundle mainBundle] loadNibNamed:#"CellTemplateFirstNewsView" owner:nil options:nil] objectAtIndex:0];
}
NSURL *urlRowImage = [NSURL URLWithString:[[NSString alloc]initWithFormat:#"http://webserver.de/inhalte/news/title/%#", detailDataNews.title_picture]];
NSData *dataRowImage = [NSData dataWithContentsOfURL:urlRowImage];
UIImageView *firstNewsImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 320, 160)];
firstNewsImageView.backgroundColor = [UIColor clearColor];
firstNewsImageView.opaque = NO;
firstNewsImageView.image = [UIImage imageWithData:dataRowImage];
cell.backgroundView = firstNewsImageView;
// [...]
return cell;
}else{
cellName = #"MainCell";
CellTemplateNews *cell = [tableView dequeueReusableCellWithIdentifier:cellName];
if(cell == nil){
cell = [[[NSBundle mainBundle] loadNibNamed:#"CellTemplateNewsView" owner:nil options:nil] objectAtIndex:0];
}
NSURL *urlRowImage = [NSURL URLWithString:[[NSString alloc]initWithFormat:#"http://webserver.de/inhalte/news/cover/%#", detailDataNews.cover_picture]];
NSData *dataRowImage = [NSData dataWithContentsOfURL:urlRowImage];
UIImage *rowImage = [UIImage imageWithData:dataRowImage];
cell.thumbImage.image = rowImage;
// [...]
return cell;
}
}r
I think the main problem is the synchronous network request that's happening for every cell, whether it's reused or not. The culprit is dataWithContentsOfURL:.
Check out this apple sample code, or google the phrase "ios lazy table images". In this SO answer, I provide a fairly simple to implement method that takes care of the loading an image asynch, then finding and updating the correct table view row upon completion.
One posibility is that you've forgotten to enter the CellIdentifier in your Xib. In fact the reuseIdentifier method needs your cell to have the same identifier, but you never set it when you create your cell. Those cells in your Xib have to have their identifier entered.
Do not alloc-init any object in cellForRowAtIndexPath You are probably leaking massive memory. Remember, this method called every time a cell comes into view, even cells that have been previously shown.
Also, your assignments to string variables are redundant. It is sufficient to do it like this.
BOOL firstCell = (indexPath.row==0 && indexPath.section==0);
NSString *cellIdentifier = firstCell ? #"FirstMainCell" : #"MainCell";
UITableViewCell *genericCell = [tableView
dequeueReusableCellWithIdentifier:cellIdentifier];
if (firstCell) {
cell = (CellTemplateFirstNews*) genericCell;
// configure...
}
else {
cell = (CellTemplateNews*) genericCell;
// configure...
}
return cell;
The other possible reason for your lag is the synchronous call to the web service. You are doing this each time the cell becomes visible. You need to load the images lazily and update the UI when necessary.
I have, in my main tableView, some custom cells (cells with an imageView, basically).
The imageView is set only if a value on my plist is false. Then, when it's true, the imageView is nil.
Basically when the user enters the detailView, the value is set to YES and the cell.imageView is nil.
And it's okay, it works
I'm using a searchDisplayController, when i search for something that has a cell.imageView, going into the detailView and then coming back to the searchResultsTable, the cell has still the image, while it shouldn't, but the main tableView has the correct cell (so, with no image).
I thought that it could depend on searchResultsTableView, but i'm not sure.
I tried with
[self.searchDisplayController.searchResultsTableView reloadData];
with no effect.
How could i reload the searchResultsTableView so that it shows the right cells, those with the image and those that don't have the image anymore?
Any help appreciated!
EDIT
This is my cellForRowAtIndexPath method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
NSArray *rows;
if (tableView == self.searchDisplayController.searchResultsTableView) {
rows = filteredList; //for search
} else {
NSDictionary *section = [localSortedTips objectAtIndex:indexPath.section];
rows = [section objectForKey:#"Rows"];
}
NSDictionary *item = [rows objectAtIndex:indexPath.row];
cell.textLabel.text = [item objectForKey:#"name"];
if ([[item valueForKey:#"isRead"] boolValue] == NO) {
cell.imageView.image = [UIImage imageNamed:#"unread.png"];
} else {
cell.imageView.image = nil;
}
cell.textLabel.font = [UIFont boldSystemFontOfSize:15.0];
cell.textLabel.adjustsFontSizeToFitWidth = YES;
return cell;
}
If I understood you right, then you can have a workaround but searching again with the same search string:
if (self.searchDisplayController.active) {
self.searchDisplayController.searchBar.text = self.searchDisplayController.searchBar.text;
}
put it in viewWillAppear: or viewDidAppear: which will be called each time the view is shown up (eg. you go back from the detail view to your search view). And reloading the data in this place would be nice too, to get the right data (for example if you marked the cell as read like in your sample code)
Just [self.tableView reloadData]; and not the searchResultsTableView (it will be automatically use the updated data after the new search)
It sounds as if perhaps your cells are being recycled and not reset properly. UITableView uses view recycling, so it's important that if you do something such as set an image you make sure it is explicitly set even when their isn't an image to display.
If you share your cellsForRowAtIndexPath code you might be able to get some more help.
I have and array of strings which is displayed in uitableview. When user taps on sort button the array is sorted and then i use [tableview reloaddata]. so that new sorted contents are displayed in table. but when i select particular cell the cell shows two texts overlapped on each other,the new sorted text and the text previously present in that cell.why is it happening so.
This is my code to display cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] ;
}
UILabel * timeLabel = [[UILabel alloc]initWithFrame:CGRectMake(190, 0, 120, tableView.rowHeight)];
timeLabel.text = [[dataArray objectAtIndex:indexPath.row] time];
[cell.contentView addSubview:timeLabel];
return cell ;
}
This is my code for sorting.
-(void)sortByTime{
NSSortDescriptor *sortDescriptor;
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"time"
ascending:NO] ;
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSArray *sortedArray;
dataArray = [sql getTableData]; // get data from sql file
sortedArray = [dataArray sortedArrayUsingDescriptors:sortDescriptors];
dataArray = [[NSMutableArray alloc]initWithArray:sortedArray];
[dataTableView reloadData];
}
there is a problem in your code. You are using reusable cells, and the problem is that you are not re using the views inside your cell. Particulary, timeLabel. You are creating a new timeLabel every time you use a cell, and when you reuse one, you add an adicional label to your cell, that is the possible reason for the ovelapped text.
For reuse the label, you should set a TAG number for the UILabel, and before create a new uilabel, check if the cell already have one.
I would replace the code:
UILabel * timeLabel = [[UILabel alloc]initWithFrame:CGRectMake(190, 0, 120, tableView.rowHeight)];
timeLabel.text = [[dataArray objectAtIndex:indexPath.row] time];
[cell.contentView addSubview:timeLabel];
with:
UILabel * timeLabel = [cell viewWithTag:55]
if(!timeLabel) {
timeLabel = [[UILabel alloc]initWithFrame:CGRectMake(190, 0, 120, tableView.rowHeight)];
timeLabel.tag = 55;
[cell.contentView addSubview:timeLabel];
}
timeLabel.text = [[dataArray objectAtIndex:indexPath.row] time];
The value for the tag number is up to you, i just use 55 as an example. Good Luck!
as you want a customized cell and you just put new subviews in cell every time this is happend you have to use customized cell created in deferent nib and use it to display your data
just make new nib and load it from new nib so you got proper data
or can check is there any subview in cell's contentView then remove them first and then add new created subview
I'm trying to use a UITextField inside a UITableViewCell as you can see in the code below. It seems that when the tableview goes off screen some data that are supposed to be in the cells are mixed up. I would think that there is some problem going on with the method [tableView dequeueReusableCellWithIdentifier:addGroupContactCellIdentifier]; not being able to give me a "proper" cell after the tableview has gone off screen. What is the reason for this?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *addGroupContactCellIdentifier = #"AddGroupContactCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:addGroupContactCellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:addGroupContactCellIdentifier];
if ([indexPath section] == 0) { // Group Name Section
cell.textLabel.text = #"Name";
UITextField *groupNameTextField = [[UITextField alloc]initWithFrame:CGRectMake(80, 10, 210, 22)];
groupNameTextField.textAlignment = UITextAlignmentLeft;
groupNameTextField.backgroundColor = [UIColor clearColor];
groupNameTextField.placeholder = #"Type Group Name";
//groupNameTextField.borderStyle = UITextBorderStyleLine;
groupNameTextField.clearButtonMode = UITextFieldViewModeWhileEditing;
groupNameTextField.returnKeyType = UIReturnKeyDone;
groupNameTextField.autocapitalizationType = UITextAutocapitalizationTypeSentences;
groupNameTextField.delegate = self;
[cell.contentView addSubview:groupNameTextField];
}
}
if ([indexPath section] == 1) { // Contacts Section
cell.textLabel.text = [[self.selectedPeoplePickerContacts objectAtIndex:[indexPath row]] objectForKey:#"name"];
cell.detailTextLabel.text = [[self.selectedPeoplePickerContacts objectAtIndex:[indexPath row]] objectForKey:#"number"];
}
cell.accessoryType = UITableViewCellAccessoryNone;
return cell;
}
UPDATE:
So I subclassed UITableViewCell but still it exhibits the same error as before. This is now my code for tableView:cellForRowAtIndexPath::
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *addGroupContactCellIdentifier = #"AddGroupContactCell";
if ([indexPath section] == 0) {
UITableViewCellWithUITextField *cell = [tableView dequeueReusableCellWithIdentifier:addGroupContactCellIdentifier];
if (cell == nil) {
//cell = [[UITableViewCellWithUITextField alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:addGroupContactCellIdentifier];
cell = [[UITableViewCellWithUITextField alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:addGroupContactCellIdentifier textFieldPlaceholder:#"Type Group Name" textFieldDelegate:self];
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.textLabel.text = #"Name";
// Need to set the UITableViewCell's textLabel properties otherwise they will cover the UITextField
cell.textLabel.opaque = NO;
cell.textLabel.backgroundColor = [UIColor clearColor];
return cell;
} else {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:addGroupContactCellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:addGroupContactCellIdentifier];
}
cell.textLabel.text = [[self.selectedPeoplePickerContacts objectAtIndex:[indexPath row]] objectForKey:#"name"];
cell.detailTextLabel.text = [[self.selectedPeoplePickerContacts objectAtIndex:[indexPath row]] objectForKey:#"number"];
return cell;
}
}
Third EDIT (I have now 2 different reuseIdentifiers which seem to give me my wanted results):
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([indexPath section] == 0) { // Group Name Section
static NSString *groupNameCellIdentifier = #"GroupNameCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:groupNameCellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:groupNameCellIdentifier];
cell.textLabel.text = #"Name";
UITextField *groupNameTextField = [[UITextField alloc]initWithFrame:CGRectMake(80, 10, 210, 22)];
groupNameTextField.textAlignment = UITextAlignmentLeft;
groupNameTextField.backgroundColor = [UIColor clearColor];
groupNameTextField.placeholder = #"Type Group Name";
//groupNameTextField.borderStyle = UITextBorderStyleLine;
groupNameTextField.clearButtonMode = UITextFieldViewModeWhileEditing;
groupNameTextField.returnKeyType = UIReturnKeyDone;
groupNameTextField.autocapitalizationType = UITextAutocapitalizationTypeSentences;
groupNameTextField.delegate = self;
[cell.contentView addSubview:groupNameTextField];
}
// Customization
return cell;
} else {
static NSString *addGroupContactCellIdentifier = #"AddGroupContactCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:addGroupContactCellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:addGroupContactCellIdentifier];
}
// Customization
cell.textLabel.text = [[self.selectedPeoplePickerContacts objectAtIndex:[indexPath row]] objectForKey:#"name"];
cell.detailTextLabel.text = [[self.selectedPeoplePickerContacts objectAtIndex:[indexPath row]] objectForKey:#"number"];
return cell;
}
}
Subclassing is not necessary as some have suggested.
But you cannot use logic like "if ([indexPath section] == 0) {" inside of the "if (cell == nil) {" because that is only called the first time the cell is created, and it will be-re used at other indexes on subsequent recycles.
Instead, you need to use two different CellIdentifiers, so that cells you have set up for section zero do not get re-used at other places in the table. Put your if ([indexPath section] == 0) { before you dequeue the cell and use a different cell identifiers for section zero and subsequent section cells.
Also, make sure you do any indexpath-specific outside of the "if (cell == nil) {" so that it will be applied each time the cell is re-used not just the first time it is created.
You are right ! The problem is definitely due to the reusability feature of UITableView. Apple has done it in such a was so that you can reuse cells, and it works beautifully at a performance stand-point ! And so, when you try scrolling up and down, and indexPath values continue to be the same and your tableView gets data from the cellForRowAtIndexPath that you had defined in your class !
Solution:
You will need to subclass your UITableViewCell and add a UITextField in your -(void)layoutSubviews method.
Then you will need to reference this CustomUITableViewCell and use that to load your TableView.
A link that will help : Read this !
The values are mixed up because when you go offscreen and then reload the table again the cells are dequed from the internal table cells pool but they are not reloaded in the same order they were in the table previously. Note that this mixing will happen even if you have a table with many rows and you scroll it. The solution is to store your textfield data in a "data source" array and then configure the cell.
EXPLANATION
Basically in your code there is one main conceptual flaw: once you have regenerated the cell, you don't configure the content properly (you don't configure it at all). What I mean is that initially, when the table is displayed the first time, the pool is empty. So each new cell that needs to be displayed is recreated from scratch (not dequed from the pool); let's say your table can show 10 cells on screen, so the first 10 cells will be all created from scratch with empty text fields.
Then you start entering text in these fields, and all works correctly.
At a certain point you start scrolling the cell: what happens is that all cells that are in the top will disappear from screen and stored (queued) in the table pool, with their textfield and its edited content; let's say you queue cell at row 0. When a new cell needs to be displayed on bottom of the screen the first thing your code does is to try to deque a cell. Now this time you have a cell in the pool (the cell that was at row 0), this cell is retrieved from the pool and placed in the table, INCLUDED THE TEXTFIELD CONTENT, at row 11. So "magically" you will find a text edited at row 0 in another row, 11. Besides the cells are retrieved in a sparse order from the pool, so after many textfield editings and scrollings you will have a complete mixup.
Solution, and this is the reason of the bug in your code: as soon as the cell has been created or dequed, configure it, that is set the textfield content. How to retrieve the textfield content? store in an array. This is why your view controller is a "data source", because you source data to fill the table. Storing data in the table is a mistake, due to this dequeing mechanism. Example:
groupNameTextField.text=[myTextFieldContentArray objectAtIndex:indexPath.row];
Another solution, but I don't suggest it, is to assign a unique identifier to each cell, that is:
NSString *myCellId = [NSString stringWithFormat:#"CellID_%d_%d",indexPath.section,indexPath.row];
In this case all cells will be enqued with a different name and you will never mix up them.
This solution is working most of the time but it is discouraged for two reasons:
a. it is non-optimal, as you don't reuse cells and so this takes extra memory for similar cells
b. you're not guaranteed that each cell is effectively queued, all in all this logic is inside the table and it's not exposed to the developer, so it may happen that you need to re-generate each time the cell when needed (performance loss).