In my UITableView Controller I have cells which each contain a UILabel and UISegmentedController. There are about 20 cells in total. The user can select the UISegmentedControl to control whether object is contained in an array or not.
My problem is that as the user scrolls the UISegmentedControl is duplicated and appears as though there are multiple selections for UISegmentedControl. I have a feeling that this is because cells are being reused but unfortunately I don't have a very good understanding of reusing cells.
I have been trying to play around with:
if(cell == nil){}
but I am not sure what should happen there:
Anyway here is my code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"featuresCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NSString *feature = [features objectAtIndex:indexPath.row];
UILabel *titleLabel = (UILabel *)[cell viewWithTag:9999];
titleLabel.text = feature;
UISegmentedControl *segmentedControl = [[UISegmentedControl alloc]initWithItems:[NSArray arrayWithObjects:#"Yes", #"No", nil]];
segmentedControl.frame = CGRectMake(215, 17, 85, 28);
[segmentedControl addTarget:self action:#selector(valueChanged:) forControlEvents: UIControlEventValueChanged];
segmentedControl.tag = indexPath.row;
[cell addSubview:segmentedControl];
if ([selectedFeatures containsObject:feature]) {
segmentedControl.selectedSegmentIndex = 0;
}
else{
segmentedControl.selectedSegmentIndex = 1;
}
return cell;
}
Your help will be greatly appreciated,
Thanks for your time.
cell is never nil so that's not going to help you.
The trick is to mark the cell in some way so as to know whether the segmented control has already been added. The best way is to use the segmented control's tag. Unfortunately you seem to be using the tag for some other purpose, so you'll need to stop doing that:
if (![cell viewWithTag:12345]) {
UISegmentedControl *segmentedControl = // ....;
// ....
segmentedControl.tag = 12345;
[cell.contentView addSubview:segmentedControl];
}
And now, you see, you are guaranteed that there is exactly one segmented control in the cell, and you can find it by calling [cell viewWithTag:12345].
The general rule is that each cell subclass should implement - (void)prepareForReuse to make it ready for configuration in - (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath. I think you can solve your problem by subclassing UITableViewCell, creating each cell with the required UISegmentedControl, and providing a method to configure the segmented control as required.
prepareForReuse should reset the segmented control so that each time you configure your custom cell it is in a known, empty state.
The advantage of this approach is that you are not only reusing the table view cell, but also the segmented control and any other decoration you want the cell to contain. Additionally you have moved the configuration of the cell into the cell object.
Briefly, in the cell subclass init method you create the segmented control and add it as a subview of the contentView.
In the cell subclass configuration method you provide you set the segmented control's segments etc.
In the cell subclass prepareForReuse you remove all those segments and leave the segmented control in a state ready for configuration next time you reuse that cell.
To go even further, start using auto layout to position the segmented control in the cell rather than using a set of hard numbers in a frame. If you get those working on one kind of device that's great but as soon as your cells have different dimensions (in a table view on a different device) things will not look so good.
Try this,
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"featuresCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NSString *feature = [features objectAtIndex:indexPath.row];
UILabel *titleLabel = (UILabel *)[cell viewWithTag:9999];
titleLabel.text = feature;
UISegmentedControl *segmentedControl;
if ([cell.contentView viewWithTag:kSegmentTag]) { //kSegmentTag a tag
segmentedControl = (UISegmentedControl *)[cell viewWithTag:kSegmentTag];
} else {
segmentedControl = [[UISegmentedControl alloc]initWithItems:[NSArray arrayWithObjects:#"Yes", #"No", nil]];
segmentedControl.frame = CGRectMake(215, 17, 85, 28);
[segmentedControl addTarget:self action:#selector(valueChanged:) forControlEvents: UIControlEventValueChanged];
segmentedControl.tag = kSegmentTag;
[cell.contentView addSubview:segmentedControl];
}
if ([selectedFeatures containsObject:feature]) {
segmentedControl.selectedSegmentIndex = 0;
}
else{
segmentedControl.selectedSegmentIndex = 1;
}
return cell;
}
and in valueChanged:
- (IBAction)valueChanged:(id)sender {
UISegmentedControl *segmentedControl = (UISegmentedControl *)sender;
CGPoint tablePoint = [segmentedControl convertPoint:segmentedControl.bounds.origin toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:tablePoint];
//here indexpath.row give changed row index
}
Related
I have a table view with a header section and inside there is a segmented control. When I select the second segment, after milliseconds, it returns automatically to the first segment. Why?
My code:
-(UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
static NSString *CellIdentifier = #"Header";
UITableViewCell *headerView = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (headerView == nil){
[NSException raise:#"headerView == nil.." format:#"No cells with matching CellIdentifier loaded from your storyboard"];
}
UISegmentedControl *segmentedControl = (UISegmentedControl *)[headerView viewWithTag:123];
[segmentedControl addTarget:self action:#selector(typeChanged:) forControlEvents:UIControlEventValueChanged];
return headerView;
}
-(void)typeChanged:(id)sender{
UISegmentedControl *segment=(UISegmentedControl*)sender;
NSLog(#"index %li",(long)segment.selectedSegmentIndex);
}
Thanks
I suggest not code in this way, some kind like hack code.
See your code, when the tableView refresh the section header, it call the function, and headerView won't be nil, because tableView create a new one for you, obviously its selection is the first segment.
I have a master detail app in ios, with SDK 7.0, Xcode 5, using ARC.
master has many items, detail has a table view. When I click on an item, the contents of tableview will change. This works well until I put a UITextField in each cell, because I want to edit the contents in the table.
The problem is: when I click on a new item, the old contents don't disappear,so the contents of a cell is a superposition of the new UITextField's text and the old UITextField's text.
The first normal tableview like this:
After I click on an new item, the tableview will like this:
The snippet of codes of master is:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
LHBPoetry *poetry = poetryArray[indexPath.row];
self.detailViewController.poetryId = poetry.poetryId;
}
I have tried a lot of things in the above method, for example, I make all instances of the detail view controller to be nil; table view's data array removeAllObejects; table view reloadData; It can't fix the problem.
The snippet of detail is:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"detailCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
UITextField *textField = textField = [[UITextField alloc] initWithFrame:
CGRectMake(90, 12, 200, 25)];
textField.tag = indexPath.row;
textField.text =_sentenceArray[indexPath.row];
textField.clearsOnBeginEditing = NO;
textField.delegate = self;
textField.returnKeyType = UIReturnKeyDone;
[textField addTarget:self
action:#selector(textFieldDone:)
forControlEvents:UIControlEventEditingDidEnd];
[cell.contentView addSubview:textField];
textField.text = _sentenceArray[indexPath.row];
return cell;
}
I draw this tableview in Main.storyborad, It has a prototype cell with an identifier.
Any help will be appreciated.
k there is something i want to tell, wy because u are keep on adding the textfields for reused cells, there is not one textfield in the cell ..:) there are more then one text field's, because of that u are getting overlapped with one other, i think u are using default "master- detail" application, and modifying it..:)
oky for that u need to modify like below
in master controller
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if(cell == nil)
{
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"];
UITextField *textField = [[UITextField alloc]initWithFrame:CGRectMake(2, 3, 300, 30)];
[textField addTarget:self action:#selector(textFieldDone:) forControlEvents:UIControlEventEditingDidEnd]; //hear u are adding once initially
textField.tag = 100;
[cell addSubview:textField];
}
NSString *object = _objects[indexPath.row];//_objects is mutable array holding the sentences or names
UITextField *textField = (UITextField *)[cell viewWithTag:100];//after that u are reusing the textfields
textField.text = object;
textField.tag = indexPath.row;
return cell;
}
now you are creating the cell thats wy u dont want the prototype cell remove it from story board
in the above u removed the custom cell becz u are creating the cell in the code it self
now in the method
- (void) textFieldDone:(UITextField *)inTextFIeld
{
int index = inTextFIeld.tag;
[_objects replaceObjectAtIndex:index withObject:[inTextFIeld text]];
[self.masterTableVIew reloadData];//made connection to ur tableview
}
I am wondering whether someone has had similar problem that I am experimented lately.
Let me describe you a little bit further about my issue. I have got a UITableViewController where I have designed a set of custom UITableViewCell in the IB. Each UITableViewCell has got different elements like a UIButton, UITextField and UILabel, etc. Obviously, each UITableViewCell has a different identifier.
Once defined all my UITableViewCells, next step is to instantiate the cell on tableView:cellForRowAtIndexPath:. What I do in this method is depending on section and row, I instantiate the different UITableViewCells by dequeueReusableCellWithIdentifier:forIndexPath:.
Everything seems to work perfectly and correctly created on the table, but the problem arrives when I scroll down. At the very top, I have got a UITableViewCell with a UIButton that I have specified with a concrete action to perform when it is clicked. Scrolling down, there are a couple of UITableViewCells with the same format (an UIButton inside with different actions specified).
The problem is, when a click the first button from the bottom side, this button performs the first action that I have defined on the very top UIButton and its own action.
It seems that when uitableviewdelegate creates new cells or reuse them, it nests functionalities from other indexPath instead of the specified indexPath....
Hope that I have explained myself properly.
Thank you in advance.
[EDIT]
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [[UITableViewCell alloc] init];
NSLog(#"indexpath value is: %#", indexPath);
if (indexPath.section == 0)
cell = [self buildSection_0:tableView getIndexPath:indexPath];
else if (indexPath.section == 1)
cell = [self buildSection_1:tableView getIndexPath:indexPath];
else if (indexPath.section == 2)
cell = [self buildSection_2:tableView getIndexPath:indexPath];
else if (indexPath.section == 3)
cell = [self buildSection_3:tableView getIndexPath:indexPath];
else if (indexPath.section == 4)
cell = [self buildSection_4:tableView getIndexPath:indexPath];
return cell;
}
-(UITableViewCell *)buildSection_0:(UITableView *)tableView getIndexPath:(NSIndexPath *)indexPath{
NSString *identifier;
UITableViewCell *cell;
if (indexPath.row == 0){
identifier = #"headerCell";
cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
}
else if (indexPath.row == 1){
identifier = #"buttonCell";
cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
UIButton *button = (UIButton *)[cell viewWithTag:3001];
[button setTitle:#"Scan" forState:UIControlStateNormal];
[button addTarget:self action:#selector(scan) forControlEvents:UIControlEventTouchUpInside];
}
return cell;
}
-(UITableViewCell *)buildSection_3:(UITableView *)tableView getIndexPath:(NSIndexPath *)indexPath{
NSString *identifier;
UITableViewCell *cell;
if (indexPath.row == [[job jobLocations] count]) { // Adding button insert more Locations
identifier = #"buttonCell";
cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
UIButton *button = (UIButton *)[cell viewWithTag:3001];
[button setTitle:#"Add Location" forState:UIControlStateNormal];
[button addTarget:self action:#selector(newLocation) forControlEvents:UIControlEventTouchUpInside];
}
else{ // Showing different Locations
identifier = #"locationCell";
cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
UILabel *label = (UILabel *)[cell viewWithTag:3007];
UITextField *textField = (UITextField *)[cell viewWithTag:3008];
[label setText:[NSString stringWithFormat:#"Location #%ld", (long)indexPath.row + 1]];
[textField setText:[[[job jobLocations] objectAtIndex:indexPath.row] locationType]];
}
return cell;
}
dequeueReusableCellWithIdentifier:forIndexPath will reuse cells that are no longer visible, you may want to implement prepareForReuse on your cell, or reset it after you dequeue it.
[EDIT]
Seeing you added your code, you should remove previous target/actions from the button before adding the new ones, as the old one will still be there if a cell is being reused:
[button removeTarget:self action:NULL forControlEvents:UIControlEventTouchUpInside];
[button addTarget:...
Add a tag to the button that corresponds to the indexPath.row. Then in your button action method, you can determine which button was clicked by looking at the (UIButton *)sender's tag.
I am currently building an iOS app that uses a UITableView with custom UITableViewCells that include a button. What I want to accomplish is having the UIButton's image change on touch up inside. That part is working fine, the issue is when you scroll, suddenly every few row buttons located in the UITableViewCell have this new image. Below is some of my code. Any help would be appreciated.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *cellID = #"CellID";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if(cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellID];
//YEP BUT
yepBut = [[UIButton alloc] initWithFrame:CGRectMake(23, 16, 64, 32.5)];
[yepBut addTarget:self action:#selector(yepPost:) forControlEvents:UIControlEventTouchUpInside];
[cell.contentView addSubview:yepBut];
}
[yepBut setImage:[UIImage imageNamed:#"yepBtn"] forState:UIControlStateNormal];
return cell;
}
-(void) yepPost:(id) sender{
//UIButton *clicked = (UIButton *) sender;
[sender setImage:[UIImage imageNamed:#"yepBtnACT"] forState:UIControlStateNormal];
}
Thanks!
You need to have a property that you set when the button is touched that you check in cellForRowAtIndexPath. So, in the button's method, have a property, say lastTouched (#property (strong,nonatomic) NSIndexPath *lastTouched, or NSInteger if you don't have sections), that you set to the indexPath (or indexPath.row) of the cell in which the touched button resided (gotten from the cell which you get by searching up through the superviews of the button until you find the cell, or by using tags on your buttons equal to the indexPath.row). After that, you have to reloadData or reloadRowsAtIndexPaths to make the change happen. In cellForRowAtIndexPath, you would set your images like this:
if ([self.lastTouched isEqual:indexPath]) {
[yepBut setImage:[UIImage imageNamed:#"yepBtnACT"] forState:UIControlStateNormal];
else{
[yepBut setImage:[UIImage imageNamed:#"yepBtn"] forState:UIControlStateNormal];
}
When you first initialize lastTouched, you want to set it to an indexPath that doesn't occur in your table, so nothing will have the yepButAct image until you touch one.
Subclassing UITableViewCell fixed my issue.
Hi I have a UITableView and I am dymanically inserting cells into it that contain a UIStepper and a UILabel. The UILabel shows the value of the UIStepper.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
if(!cell)
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"cell"];
[cell.textLabel setText:[self.myarray objectAtIndex:indexPath.row]];
UIStepper *stepper = [[UIStepper alloc]init];
UILabel *label = [[UILabel alloc]init];
label.text = [NSString stringWithFormat:#"%.f", stepper.value];
[cell addSubview:stepper];
[cell addSubview:label];
[stepper addTarget:self action:#selector(incrementStepper:) forControlEvents:UIControlEventValueChanged];
return cell;
}
I have removed some of the above lines that do the formatting for clarities sake but this works and everything is ok.
-(void)incrementSkillStepper:(id)sender
{
UIStepper *stepper = (UIStepper*)sender;
//set the label that is in the same cell as the stepper with index stepper.value.
}
When I click the stepper in the specific cell I want the label in the same cell to increment, but my problem is in which the way that addtarget works - I can only send the sender which in this case is the stepper to the event which means it doesnt have access to the dynamically created label. Does anybody know how I can set the text of the label from the incrementStepper delegate method?
Call [sender superview] to get the cell that it's in,
-(void)incrementSkillStepper:(id)sender
{
UIStepper *stepper = (UIStepper*)sender;
UITableViewCell* cell = [stepper superview];
UIView* subview = [[cell subviews] lastObject]; // Make sure your label *is* the last object you added.
if ([subview isKindOfClass:[UILabel class]]) {
// do what you want
}
}
you can also loop thru the [cell.contentView subviews] array, and get the label you need, better give the label a tag value, and use viewWithTag
You can set tag to UIStepper as indexPath.row and set tag to label as indexPath.row+999.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIStepper *stepper = [[UIStepper alloc]init];
stepper.tag = indexPath.row;
UILabel *label = [[UILabel alloc]init];
label.tag = indexPAth.row + 999;
//......
}
Now in delegate method of UIStepper, you can find label in that cell like this
-(void)incrementSkillStepper:(id)sender
{
UIStepper *stepper = (UIStepper*)sender;
UILabel *label = (UILabel *)[self.view viewWithTag:sender.tag + 999];
//now this is same label as you want. Now you can change the value of label as you want
}
You should update the model with the value of the stepper, and then set the label's value based on that value in the model. You should give the stepper a tag in cellForRowAtIndexPath that's equal to the indexPath.row, and in the stepper's action method, set the value of a property in your model to the stepper's value. Then, reload the row at that same indexPath.
(void)incrementSkillStepper:(UIStepper *)sender {
NSInteger row = sender.tag;
[self.theData[row] setObject:#(sender.value) forKey:#"stepperValue"];
[self.tableView reloadRowsAtIndexPaths: #[row] withRowAnimation:UITableViewRowAnimationNone];
}
In cellForRowAtIndexPath, you would have something like this to populate the label:
UILabel *label = [[UILabel alloc]init];
label.text = [NSString stringWithFormat:#"%#", self.theData[indexPath.row][#"stepperValue"]];
In this example, I'm assuming you have an array of dictionaries (theData) that has all the data you need to populate the cells. One of the dictionary keys, "stepperValue" would be used to store the stepper value as an NSNumber.