9

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];

}
user3926564
  • 105
  • 1
  • 1
  • 6

6 Answers6

12

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];
    }
}
staticVoidMan
  • 19,275
  • 6
  • 69
  • 98
  • Ohh I see. It seems to be working correctly now. one question I do have though is are you actually reusing the cells or are you making a new cell each time? I am asking because you didnt include `cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];` and was wondering why? assuming that thats the method for initiating reusing cells. – user3926564 Aug 12 '14 at 21:30
  • @user3926564 : sorry, i guess i should have mentioned a bit about `-dequeueReusableCellWithIdentifier:`. Anyway, here goes... `-dequeueReusableCellWithIdentifier:` will return a cell which **CAN** be *reused*. In `XIB` when **NO** cell is *reusable*, it will return **nil** and we need to alloc/init a new cell for use. Where as in a storyboard setup, a cell is automatically created for us when needed and hence we don't have to manually alloc/init the cell. – staticVoidMan Aug 13 '14 at 05:24
  • @user3926564 : also... `[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];` will create a new instance of cell and does **NOT** have anything to do with the reuse logic itself. The reuse logic lies in `-dequeueReusableCellWithIdentifier:`. In XIB when a reusable cell does not exist, we alloc/init it manually. After a time, `if (cell == nil)` does not happen so this alloc/init thing happens only as many times as sufficient for the reuse logic to work. – staticVoidMan Aug 13 '14 at 05:25
  • Thanks man. Your approach was the best. I appreciate it. Please excuse my "rookieness". Just got into IOS – user3926564 Aug 13 '14 at 23:37
  • @user3926564 : well... half the work was done already by you. Just a few loose ends needed to be tied up. However, I would recommend you create your views button/label through the IB (storyboard) after you get the hang of doing it programmatically. anyways... glad it works for you :) – staticVoidMan Aug 14 '14 at 05:04
  • Hello everyone ... I have been following the guidelines in this post but I have a problem ... int i = NO;      for (i = 0; i <20; i ++) {          [self.buttonPressedSelectedRowArray addObject: [NSNumber numberWithInt: NO]];      } I have a list of users registered to my app in my tableview so I can not give a "1 <20" because the total number of cells are in constant change .. how can I avoid that in addition to the 20 cells the app crashes? – kAiN Sep 22 '14 at 16:49
  • @staticVoidMan i define button on storyboard...so is it makes any difference – Bhavin Bhadani Apr 07 '15 at 11:09
  • @Bhavin it should work with storyboard implementation as well – staticVoidMan Apr 09 '15 at 14:54
3

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];
Marko Zadravec
  • 8,298
  • 10
  • 55
  • 97
2

To resuse the cell , put your cell identifier value in interface builder .

Akshay Mehta
  • 213
  • 1
  • 8
1

First of all you are always setting @(1)([NSNumber numberWithInt:1], better to use @(YES) or @(NO) for this) to your array when you tap cell button. That's why you'll see incorrect results. Then you are always adding button to your cells, so if it's reused 10 times, you'll add 10 buttons there. It's better to use xib or storyboard prototype for that.

Timur Kuchkarov
  • 1,155
  • 7
  • 21
1

Swift 2.1 version of accepted answer. Works great for me.

        override func viewDidLoad() {
        super.viewDidLoad();

        self.arrayProducts = NSMutableArray(array: ["this", "that", "How", "What", "Where", "Whatnot"]); //array from api for example
        var i:Int = 0;
        for (i = 0; i<=self.arrayProducts.count; i++){
            let numb:NSNumber = NSNumber(bool: false);
            self.arrayFavState.addObject(numb); //array of bool values
            }
        }

TablView Datasource methods ::

        func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("SubCategoryCellId", forIndexPath: indexPath) as! SubCategoryCell;


        cell.btnFavourite.addTarget(self, action: "btnFavouriteAction:", forControlEvents: UIControlEvents.TouchUpInside);

        var isFavourite: Bool!;
        isFavourite = self.arrayFavState[indexPath.row].boolValue;
        if isFavourite == true {
            cell.btnFavourite.setBackgroundImage(UIImage(named: "like-fill"), forState: UIControlState.Normal);
        } else {
            cell.btnFavourite.setBackgroundImage(UIImage(named: "like"), forState: UIControlState.Normal);
        }


        return cell;
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.arrayProducts.count;
    }

Button Action method ::

        func btnFavouriteAction(sender: UIButton!){

        let position: CGPoint = sender.convertPoint(CGPointZero, toView: self.tableProducts)
        let indexPath = self.tableProducts.indexPathForRowAtPoint(position)
        let cell: SubCategoryCell = self.tableProducts.cellForRowAtIndexPath(indexPath!) as! SubCategoryCell
        //print(indexPath?.row)
        //print("Favorite button tapped")
        if !sender.selected {
            cell.btnFavourite.setBackgroundImage(UIImage(named: "like-fill"), forState: UIControlState.Normal);
            sender.selected = true;
            self.arrayFavState.replaceObjectAtIndex((indexPath?.row)!, withObject:1);
        } else {
            cell.btnFavourite.setBackgroundImage(UIImage(named: "like"), forState: UIControlState.Normal);
            sender.selected = false;
            self.arrayFavState.replaceObjectAtIndex((indexPath?.row)!, withObject:0);
        }

    }
Deepak Thakur
  • 3,453
  • 2
  • 37
  • 65
0

I need to see your code, but i can say u didn't change your model or you changed it all of rows in your array.

You can use some class extended from UITableViewCell and in "cellForRowAtIndexPath" method use it as reusable cell.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
 {
   MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCell"];


    // Configure the cell...
    MyModel *model = [_models objectAtIndex:indexPath.row];
    cell.nameLabel.text = model.name;
    cell.image.image = model.image;
    cell.likeButton.text = model.likeButtonText;
    return cell;
 } 

and then in your "didSelectRowAtIndexPath" method change "model.likeButtonText".

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
   MyCell cell* =     [tableView cellForRowAtIndexPath:indexPath];
   MyModel *model = [_models objectAtIndex:indexPath.row];
   model.likeButtonText = [model.likeButtonText isEqual:@"like"]?@"unlike":@like;
   cell.likeButton.text = model.likeButtonText;
}

This will update your cell

Ali Riahipour
  • 524
  • 6
  • 20
  • Thanks @Ali RP. I updated the question with the code. I see what you did but does dedseleteRowIndexpath work for button clicks? I though that is only used for is when I tap on the cell? – user3926564 Aug 12 '14 at 06:55