I've encountered some very strange behaviour with my tableViewCells in a simple list application (iOS).
This is the basic function of my working app :
TableView with 2 sections. First section (e.g. with 2 cells) is always visible. Second section can be shown/hidden with a custom button in the section header. I've created two classes (i.e. FirstSectionItem and SecondSectionItem) both with a boolean property "completed" and some other properties.
After compilations the app runs as expected. Tapping cells results in showing the checkmarks and tapping them again hides the checkmarks (=custom imageView). However after tapping some different cells (random order) or showing/hiding the second section the checkmark (ImageViews) tend to stay checked no matter how much I tap the cells. After a while every cell is checked and can't be unchecked but the boolean values still keep changing.
Here's part of the code:
#property NSMutableArray *sectionTitleArray;
#property NSMutableDictionary *sectionContentDict;
#property NSMutableArray *firstSectionItems;
#property NSMutableArray *secondSectionItems;
ViewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
if (!self.sectionTitleArray) {
self.sectionTitleArray = [NSMutableArray arrayWithObjects:#"section1", #"section2", nil];
}
self.firstSectionItems = [[NSMutableArray alloc] init];
self.secondSectionItems = [[NSMutableArray alloc] init];
[self loadInitialData];
self.sectionContentDict = [[NSMutableDictionary alloc] init];
self.arraySection1 = [[NSMutableArray alloc] init];
for (int i = 0; i < [self.firstSectionItems count]; i++)
{
FirstSectionItem *firstSectionItem = [self.firstSectionItem objectAtIndex:i];
[self.arraySection1 addObject:firstSectionItem.itemName];
}
self.arraySection2 = [[NSMutableArray alloc] init];
for (int i = 0; i < [self.secondSectionItems count]; i++)
{
SecondSectionItem *secondSectionItem = [self.secondSectionItems objectAtIndex:i];
[self.arrayFuture addObject:secondSectionItem.itemName];
}
[self.sectionContentDict setValue:self.arraySection1 forKey:[self.sectionTitleArray objectAtIndex:0]];
[self.sectionContentDict setValue:self.arraySection2 forKey:[self.sectionTitleArray objectAtIndex:1]];
}
CellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"ListPrototypeCell" forIndexPath:indexPath];
// ... cell content ...
NSArray *content = [self.sectionContentDict valueForKey:[self.sectionTitleArray objectAtIndex:indexPath.section]];
//... constraints ...
UIImage *checkmark = [UIImage imageNamed:#"checkmark.png"];
UIImage *noCheckmark = [UIImage imageNamed:#"transparent.png"];
UIImageView *imageView = [[UIImageView alloc] init];
if (indexPath.section==0){
FirstSectionItem *firstSectionItem = [self.firstSectionItems objectAtIndex:indexPath.row];
imageView.image = firstSectionItem.completed ? checkmark : noCheckmark;
}
if (indexPath.section==1){
if(self.sec2isTapped == YES){
SecondSectionItem *secondSectionItem = [self.secondSectionItems objectAtIndex:indexPath.row];
imageView.image = secondSectionItem.completed ? checkmark : noCheckmark;
}
}
[cell.contentView addSubview:imageView];
// ... more constraints
return cell;
}
didSelectRowAtIndexPath
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
if (indexPath.section ==0){
FirstSectionItem *tappedItem = [self.firstSectionItems objectAtIndex:indexPath.row];
tappedItem.completed = !tappedItem.completed;
}
if (indexPath.section ==1){
SecondSectionItem *tappedItem = [self.secondSectionItems objectAtIndex:indexPath.row];
tappedItem.completed = !tappedItem.completed;
}
[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationMiddle];
[tableView endUpdates];
}
I've tried multiple approaches but none of them seem to work. I can't figure out what the problem is.
(I am new to objective C so some parts might seem a bit devious).
Thanks in advance.
The problem is that you are initializing the UIImageView each time cellForRowAtIndexPath is running. But remember that that cells are reused so what you are doing is reusing a cell that has a checkmark and adding a new clear checkmark on top.
What you need to do is either:
Add the imageView in the storyboard, tag it, and user the tag to find it in the code
Subclass UITableViewCell and assign the correct image in there.
There are MANY articles of how to do both online.
Question is in the title ^^. How do you go about saving say 2 textFields in a custom tableView cell. Pretend the reuse identifier doesn't exist (however is someone could show me how to get around that then that would be awesome). Thanks for anyone that can help.
In your custom UITableViewCell class you can add textfields from an array of textfields.
ie, in the viewDidLoad you can insert two textfields of tag 110 and 111 in an array.
-(void)viewDidLoad
{
arryTextfields = [NSMUtableArray alloc]init];
for(int i=0;i<2;i++)
{
UITextField * textField = [[UITextField alloc]init];
NSMutableDictionary * dic =[NSMutableDictionary alloc]init]
[dic setObject:textField forKey:[NSString stringWithFormat:#"11%d",i]];
[arryTextfields addObject:dic];
[textField release];
[dic release];
}
}
In your tableview delegate "cellForRowAtIndexPath".
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
YourCustomTableviewCell * cell = [[YourCustomTableviewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
UITextField * textfield1 = [[cell.arryTextfields objectAtIndex:indexPath.row]objectForKey:#"110"];
UITextField * textfield2 = [[cell.arryTextfields objectAtIndex:indexPath.row]objectForKey:#"111"];
return cell;
}
I have table view that rendering cells. I created the cell once and reuse it. In each cell there is a button to do vote action for a place. On the vote action I want to change the background image of the button. When I click on the button in a certain cell the background image changes, but when I scroll down I find another button with the changed background image. I believe that the problem is because I'm using dequeueReusableCellWithIdentifier but I can't find a way to solve this problem.
Here is my code:
// .h file
#interface VotePlaceView : UIView < UITableViewDataSource, UITableViewDelegate>
{
VotePlaceView *votePlaceCell;
}
and in the implementation file
// .m File
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"votePlaceCell"];
if (cell)
{
votePlaceCell = (VotePlaceView*)[cell viewWithTag:10];
}
if (!cell)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"votePlaceCell"];
//GlobalObjects is a customised thing for me to load the cell in different languages
votePlaceCell = [[GlobalObjects loadNibNamed:#"votePlaceCell" owner:self options:nil] objectAtIndex:0];
votePlaceCell.tag = 10;
[cell addSubview:votePlaceCell];
}
.
. //Fill data in the cell
.
//This gives different IDs, no ID like the others (unique ID)
NSLog(#"ID %d", [[[self.votePlacesResults objectAtIndex:indexPath.row] valueForKey:#"id"] intValue]);
UIButton* voteButton = (UIButton*)[votePlaceCell viewWithTag:20];
voteButton.tag = [[[self.votePlacesResults objectAtIndex:indexPath.row] valueForKey:#"id"] intValue];
return cell;
}
-(IBAction)votePlace:(UIButton *)sender
{
// I tried two ways to achieve my point with no luck
//Selected case give the new background image
//THE FIRST WAY
sender.selected = YES;
//OR THE SECOND WAY
UIButton* newVotePlace;
newVotePlace = (UIButton*)[[sender superview] viewWithTag:sender.tag];
newVotePlace.selected = YES;
}
Any idea?
The Solution
#Akhilrajtr's answer solved my problem, after I made some changes to his answer. Here is the solution, in case if anyone faced the same problem.
Instead of using a UIButton, I used UIButton and UIImageView. I put the UIImageView behind the UIButton and I give the UIImageView two images in the default case and in the highlighted case. I cleared the background image of the UIButton and then followed the accepted answer as follows:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
.
.
int placeId = [[[self.votePlacesResults objectAtIndex:indexPath.row] valueForKey:#"id"] intValue];
UIButton* voteButton = (UIButton*)[votePlaceCell viewWithTag:20];
if (!voteButton) {
voteButton = (UIButton*)[votePlaceCell viewWithTag:placeId];
}
voteButton.tag = [[[self.votePlacesResults objectAtIndex:indexPath.row] valueForKey:#"id"] intValue];
if ([selectedIdArray containsObject:[[self.votePlacesResults objectAtIndex:indexPath.row] valueForKey:#"id"]]) {
UIImageView* voteImage;
voteImage = (UIImageView*)[votePlaceCell viewWithTag:11];
voteImage.highlighted = YES;
} else {
UIImageView* voteImage;
voteImage = (UIImageView*)[votePlaceCell viewWithTag:11];
voteImage.highlighted = NO;
voteButton.selected = NO;
}
return cell;
}
-(IBAction)votePlace:(UIButton *)sender
{
[selectedIdArray addObject:[NSNumber numberWithInt:sender.tag]];
UIImageView* voteImage;
voteImage = (UIImageView*)[[sender superview] viewWithTag:11];
voteImage.highlighted = YES;
}
Try this,
Create a NSMutableArray *selectedIdArray (don't forget to instatiate selectedIdArray in viewDidLoad:) to store selected ID's , and in cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
.
.
//Other codes
.
.
NSLog(#"ID %d", [[[self.votePlacesResults objectAtIndex:indexPath.row] valueForKey:#"id"] intValue]);
int placeId = [[[self.votePlacesResults objectAtIndex:indexPath.row] valueForKey:#"id"] intValue];
UIButton* voteButton = (UIButton*)[votePlaceCell viewWithTag:20];
if (!voteButton) {
voteButton = (UIButton*)[votePlaceCell viewWithTag:placeId];
}
voteButton.tag = [[[self.votePlacesResults objectAtIndex:indexPath.row] valueForKey:#"id"] intValue];
if ([selectedIdArray containsObject:[NSNumber numberWithInt:placeId]) {
voteButton.selected = YES;
} else {
voteButton.selected = NO;
}
return cell;
}
and in
-(IBAction)votePlace:(UIButton *)sender
{
[selectedIdArray addObject:[NSNumber numberWithInt:sender.tag]];
sender.selected = YES;
}
You need to store an array containing information about the background for each cell, and set the background for each cell in cellForRowAtIndexPath.
As you're using selected on the button, create an NSMutableArray with as many entries as the cells, and instead of setting the button itself to selected, change the bool on the selected index of the array, then you can set selected dependent on the value of the desired index of NSMutableArray in cellForRowAtIndexPath.
I'm trying to create a registration form that contains around 20 different fields separated in four different sections.
I've created a custom cell for that, that contains a label and a UITextField and I've got an array of dictionaries on my tableview that indicates what the name of the label should be, as well as the tag of the texftield (because depending upon the tag I use a UIPicker to enter data).
The problem is that, when I start editing the fields, scrolling down and back up everything gets messed up, and changed on fields it should not... I've came across that this is obviously happening because I'm dequeing cells so it's creating duplicated references, so I tried to create a new cell every time, but that doesn't work since it will only initialize a tableview with the exact amount of cells I need but just blank cells with no content (label nor textfield) whatsoever inside.
What would be the easiest approach so I can keep a different memory reference for each one of my cells, considering the fact that I've got to retrieve their values when a button is pressed?
I've read the suggestions on this post but still have no idea about how to create an array of cells.
Any help?
This is the code for my definitions:
NSArray *section1 = [[NSArray alloc] initWithObjects:
[NSDictionary dictionaryWithObjectsAndKeys:#"100",#"tag",#"Title",#"title",#"title",#"fieldName",nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"0",#"tag",#"First Name",#"title",#"first_name",#"fieldName",nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"0",#"tag",#"Last Name",#"title",#"last_name",#"fieldName",nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"0",#"tag",#"Job Title",#"title",#"job_title",#"fieldName",nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"0",#"tag",#"Email",#"title",#"email",#"fieldName",nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"0",#"tag",#"Confirm Email",#"title",#"confirm_email",#"fieldName",nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"0",#"tag",#"Password",#"title",#"password",#"fieldName",nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"0",#"tag",#"Confirm Password",#"title",#"conf_password",#"fieldName",nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"0",#"tag",#"Phone",#"title",#"phone",#"fieldName",nil],
nil];
....
self.formItems = [[NSArray alloc] initWithObjects:
section1,
section2,
section3,
section4,
section5,
nil];
And then the code I use on cellForRowAtIndexPath is the following:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"freeTrialFormCell";
TSIFreeTrialFormCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[TSIFreeTrialFormCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSArray* sectionData = [self.formItems objectAtIndex:indexPath.section];
NSDictionary* myFieldInfo = [sectionData objectAtIndex:indexPath.row];
if ([[myFieldInfo objectForKey:#"fieldName"] rangeOfString:#"password"].location != NSNotFound) {
cell.textField.secureTextEntry = YES;
}
cell.fieldName = [myFieldInfo objectForKey:#"fieldName"];
cell.textField.placeholder = [myFieldInfo objectForKey:#"title"];
cell.textField.tag = [[myFieldInfo objectForKey:#"tag"] integerValue];
cell.label.text = [myFieldInfo objectForKey:#"title"];
return cell;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TSIFreeTrialFormCell *cell = [[TSIFreeTrialFormCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"cell"] autorelease];
NSArray* sectionData = [self.formItems objectAtIndex:indexPath.section];
NSDictionary* myFieldInfo = [sectionData objectAtIndex:indexPath.row];
if ([[myFieldInfo objectForKey:#"fieldName"] rangeOfString:#"password"].location != NSNotFound) {
cell.textField.secureTextEntry = YES;
}
cell.fieldName = [myFieldInfo objectForKey:#"fieldName"];
cell.textField.placeholder = [myFieldInfo objectForKey:#"title"];
cell.textField.tag = [[myFieldInfo objectForKey:#"tag"] integerValue];
cell.label.text = [myFieldInfo objectForKey:#"title"];
return cell;
}
But this will have a performance hit as you are creating cell for each row
The UITableView reuses cells as you scroll up and down, so you will lose info that is only typed into that cell. What I do is create an array to hold the inputs for those cells.
Basically:
enteredText = [[NSMutableArray alloc] init]; //iVar, not local variable
for (int i=0; i<numTableRows; i++) {
[enteredText addObject:#""]; //fill with dummy values that you will replace as user types
}
Then in your table view text field, set yourself as the delegate of the text field and implement textFieldDidEndEditing. In that method, replace the value in enteredTextfor that row with the user's text.
When you make the cell, set the text field text to the value of in your array:
myCell.textField.text = [enteredText objectAtIndex:indexPath.row];
It'll be blank at first, and then will hold the user's text after they've put something in there.
I have an NSMutableArray in with data (hard coded).
I implemented these two TableView methods and in IB, I've set the delegate and datasource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [myArray count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"UITableViewCell"];
if (!cell) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"UITableViewCell"] autorelease];
}
cell.textLabel.text = [myArray objectAtIndex:[indexPath row]];
NSLog(#"Cell is %#", [myArray objectAtIndex:indexPath.row]);
return cell;
}
The data won't appear in the TableView. I've ran NSLog and I can see the data is in myArray.
I added the NSLog to the code and it appears the code never executes. The question is how is my array being created?
Here's the code
- (id)init:(NSMutableArray *)theArray
{
[super init];
countryTable.delegate = self;
countryTable.dataSource = self;
UITapGestureRecognizer * tapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(viewTapped:)] autorelease];
[window addGestureRecognizer:tapRecognizer];
myArray = [[NSMutableArray alloc] init];
myArray = theArray;
return self;
}
Your basic code is correct, just need to have an array (which you haven't provided here), so this would be one example:
NSMutableArray *myArray = [NSMutableArray arrayWithObjects:#"One",#"Two",#"Three",nil];
And you can NSLog this: NSLog(#"Count: %i, Array: %#",[myArray count], myArray);
Output: Which should return as the cell.textLabel.text per your example
Count: 3, Array: (
"One",
"Two",
"Three"
)
You should also make sure that you set the UITableViewDelegate and UITableViewDataSource protocols in your header.
I don't know if the question is still open but I give it a try.
As I read your code I can't see where countryTable is instantiated so I assume it's connected in InterfaceBuilder? And if this is the case, countryTable is only valid after viewDidLoad.
But you could set delegate and dataSource in InterfaceBuilder as well.
By the way, every once you change the datasource (myArray), call countryTable.reloadData.
Try this -
cell.textLabel.text = [[myArray objectAtIndex:indexPath.row] retain];