Ok. I cant seem to get a firm understanding on how tableviews work. Would someone please explain to me how cells are reused in tableviews especially when scrolling? One of the major pain points I have about this is the fact that when I create an action in one cell, other cells are affected when I scroll. I tried using an array as the backend for the model but still I get cells that change when not suppose to. The hard thing to figure out is why do they change when the the model in the array is not changed.
A simple example:
table view cells with the button "like". When I click the button in one of the cells, the button text changes to "Unlike"(So far so good). But When I scroll down, other cells also show "Unlike" even though I haven't selected them. And when I scroll up, the cells I originally selected change again and newer ones are changed as well.
I cant seem to figure this out. If you can show me a working example source code, that would be awesome!!! Thanks!
- (void)viewDidLoad
{
[super viewDidLoad];
likeState = [[NSMutableArray alloc]init];
int i =0;
for (i=0; i<20; i++) {
[likeState addObject:[NSNumber numberWithInt:0]];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
UIButton *myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[myButton setTitle:@"Like" forState:UIControlStateNormal];
[myButton addTarget:self action:@selector(tapped:) forControlEvents:UIControlEventTouchUpInside];
myButton.frame = CGRectMake(14.0, 10.0, 125.0, 25.0);
myButton.tag =indexPath.row;
[cell.contentView addSubview:myButton];
if (cell ==nil) {
}
if ([[likeState objectAtIndex:indexPath.row]boolValue]==NO) {
[myButton setTitle:@"Like" forState:UIControlStateNormal];
}
else{
[myButton setTitle:@"Unlike" forState:UIControlStateNormal];
}
return cell;
}
-(void)tapped:(UIButton *)sender{
[likeState replaceObjectAtIndex:sender.tag withObject:[NSNumber numberWithInt:1]];
[sender setTitle:@"Unlike" forState:UIControlStateNormal];
}
The cells of UITableView are instances of UITableViewCell or its subclasses. It is the table view that adds, removes, and arranges cells in its view hierarchy. Table views reuse cells that go out of the screen to display new elements, so that:
Reusable cells are a fundamental piece of a working table view. Not only you need to understand how cells are reused by a table view, but you also need to design properly so that the table view will resize them automatically using Auto Layout. A table view displays its elements using specialized subviews called cells.
Were you using a UITableViewController? One of the few things it does is call reloadData () on the table view. Now you have to do it yourself. Place it after you assign the data source to the table view.
Table views are more versatile than you might think. For example, many developers make their life harder using a scroll view when a UITableView would be a better choice. Finally, architecture is crucial for table views.
I am assuming you are doing this via Storyboard
and since you haven't created your button via the Interface Builder
, you need to check if the cell that is being re-used already has the button or not.
As per your current logic, you are creating a new button instance ever time the cell reappears.
I'd suggest the following:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
//following is required when using XIB but not needed when using Storyboard
/*
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
*/
//Reason:
//[1] When using XIB, dequeueReusableCellWithIdentifier does NOT create a cell so (cell == nil) condition occurs
//[2] When using Storyboard, dequeueReusableCellWithIdentifier DOES create a cell and so (cell == nil) condition never occurs
//check if cell is being reused by checking if the button already exists in it
UIButton *myButton = (UIButton *)[cell.contentView viewWithTag:100];
if (myButton == nil) {
myButton = [UIButton buttonWithType:UIButtonTypeCustom];
[myButton setFrame:CGRectMake(14.0,10.0,125.0,25.0)];
[myButton setTag:100]; //the tag is what helps in the first step
[myButton setTitle:@"Like" forState:UIControlStateNormal];
[myButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[myButton addTarget:self action:@selector(tapped:andEvent:) forControlEvents:UIControlEventTouchUpInside];
[cell.contentView addSubview:myButton];
NSLog(@"Button created");
}
else {
NSLog(@"Button already created");
}
if ([likeState[indexPath.row] boolValue]) {
[myButton setTitle:@"Unlike" forState:UIControlStateNormal];
}
else {
[myButton setTitle:@"Like" forState:UIControlStateNormal];
}
return cell;
}
-(void)tapped:(UIButton *)sender andEvent:(UIEvent *)event
{
//get index
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
CGPoint currentTouchPosition = [touch locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:currentTouchPosition];
//toggle "like" status
if ([likeState[indexPath.row] boolValue]) {
[likeState replaceObjectAtIndex:indexPath.row withObject:@(0)];
[sender setTitle:@"Like" forState:UIControlStateNormal];
}
else {
[likeState replaceObjectAtIndex:indexPath.row withObject:@(1)];
[sender setTitle:@"Unlike" forState:UIControlStateNormal];
}
}
The biggest problem is that you create button each time you update cell.
for example if you have visible 4 roes on the screen like this :
*-----------------------*
| cell A with button |
*-----------------------*
| cell B with button |
*-----------------------*
| cell C with button |
*-----------------------*
| cell D with button |
*-----------------------*
now when you scroll down so the cell A is not visible any more it get reused and placed underneeth :
*-----------------------*
| cell B with button |
*-----------------------*
| cell C with button |
*-----------------------*
| cell D with button |
*-----------------------*
| cell A with button |
*-----------------------*
but for cell A it gets called cellForRowAtIndexPath
again.
What you did is placing another button on it. So you actually have:
*-----------------------*
| cell B with button |
*-----------------------*
| cell C with button |
*-----------------------*
| cell D with button |
*-----------------------*
| cell A with 2 buttons |
*-----------------------*
You can see how you can quite soon have a lot of buttons piling up. You can fix this by storyboard as @Timur Kuchkarov suggested, or you fix your code by
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
if (cell ==nil) {
UIButton *myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[myButton setTitle:@"Like" forState:UIControlStateNormal];
[myButton addTarget:self action:@selector(tapped:) forControlEvents:UIControlEventTouchUpInside];
myButton.frame = CGRectMake(14.0, 10.0, 125.0, 25.0);
myButton.tag = 10;
[cell.contentView addSubview:myButton];
}
UIButton * myButton = (UIButton * )[cell.contentView viewWithTag:10]
if ([[likeState objectAtIndex:indexPath.row]boolValue]==NO) {
[myButton setTitle:@"Like" forState:UIControlStateNormal];
}
else{
[myButton setTitle:@"Unlike" forState:UIControlStateNormal];
}
return cell;
}
In this way you add just 1 button, if cell was not reused (so it has nothing on it).
It this way you can not rely on mutton tag number for function tapped, (I wouldn't anyway), so you have to change it.
This part is not tested :
You can check parent of the button to see witch cell it belongs.
UITableViewCell * cell = [[button superview] superview] /// superview of button is cell.contentView
NSIndexPath * indexPath = [yourTable indexPathForCell:cell];
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With