0

Code updated to working version. Thanks again for the help :)

Hey guys. I have a UITableViewController set up to use a custom cell loaded from a nib. Here's my cellForRowAtIndexPath:

    // Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *cellIdentifier = @"PeopleFilterTableViewCell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (cell == nil) {
        [[NSBundle mainBundle] loadNibNamed:@"PeopleFilterTableViewCell" owner:self options:nil];
        cell = peopleFilterTableViewCell;
        self.peopleFilterTableViewCell = nil;
    }

    cell.selectionStyle = UITableViewCellSelectionStyleNone;

    PeopleFilterTableViewCell* tableViewCell = (PeopleFilterTableViewCell *) cell;

    /* Set direct button name */
    Person* personAtRow = [directsToShow objectAtIndex:indexPath.row];
    [tableViewCell.directButton setTitle:personAtRow.name forState:UIControlStateNormal];

    /* Set direct head count */
    tableViewCell.headcountLabel.text = [NSString stringWithFormat:@"%d", personAtRow.totalHeadCount];

    UIImage* unselectedImage = [UIImage imageNamed:@"filterButton.png"];
    UIImage* selectedImage = [UIImage imageNamed:@"filterButtonClosed.png"];

    UIButton* newFilterButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    /* Set filter button image */
    if(personAtRow.filtered){
        [newFilterButton setSelected:YES];
    } else {
        [newFilterButton setSelected:NO];
    }
    tableViewCell.filterButton = newFilterButton;

    return cell;
}

This seems to work fine for me, but one issue has come up with the code after the /* set filter button image */ comment.

The filter button is a UIButton in my custom cell nib that is supposed to reflect the state of a model array containing 'Person' objects, which have a field that can be toggled to represent whether they are being filtered or not.

The way that I allow a user to update this model object is through a custom delegate method on my top level controller, which, whenever the user clicks the filter button, updates the model and the state of the button, and additionally updates a mapViewController with some data to show based on the state of the model:

- (void)updateViews:(id)sender {
    UIImage* unselectedImage = [UIImage imageNamed:@"filterButton.png"];
    UIImage* selectedImage = [UIImage imageNamed:@"filterButtonClosed.png"];

    int row = [self rowOfCellSubView:sender];

    Person* personToFilter = [self.directsToBeShown objectAtIndex:row];
    NSLog(@"Filtering person with corpId: %@, name: %@", personToFilter.corpId, personToFilter.name);
    if (personToFilter.filtered){
        //update button image
        [sender setImage:unselectedImage forState:UIControlStateNormal];
        [sender setSelected:NO];
        //add person back.
        Person* directFiltered = [self.directsToBeShown objectAtIndex:row];
        directFiltered.filtered = NO;
        NSLog(@"Filtering person with corpId: %@, name: %@, filtered: %d", directFiltered.corpId, directFiltered.name, directFiltered.filtered);

    } else {
        //update button image
        [sender setImage:selectedImage forState:UIControlStateSelected];
        [sender setSelected:YES];
        //remove person.
        personToFilter.filtered = YES;

        NSLog(@"Filtering person with corpId: %@, name: %@, filtered: %d", personToFilter.corpId, personToFilter.name, personToFilter.filtered);
    }

    [self updateSitesToShow];
    [self.mapViewController performSelectorOnMainThread:@selector(updateDisplay) withObject:nil waitUntilDone:NO];

}

My issue comes with the updating of the state for the filter button. When my app loads the tableview, everything looks fine. When I click a filter button in a certain cell row the state of the button updates correctly, and my model objects are also updating correctly since I see the expected behavior from the mapView which I'm ultimately updating.

However, the issue is that when I click on the filterButton in one cell row and then scroll down a few rows, I notice another filter button in a different cell now has the same state as the one I clicked a few rows above. If I scroll back up again, the original button I clicked 'on' now seems to be 'off' but the row below it now appears 'on'. Of course all this is affecting is the actual display of the buttons. The actual state of the buttons is consistent and working correctly.

I know this issue must have something to do with the fact that my cells are being reused, and I'm guessing somehow the same buttons are being referenced for different cell rows. I'm at a loss as to how this is happening though, since I'm creating a new filter button for each cell, whether the cell is reused or not, and resetting the filterButton property of the cell to be the newly created object. Notice for example that the text of the headCount property, which is a UILabel also defined in the cell nib, is also being reassigned to a new String object for each cell, and it is displaying correctly for each row.

I've been struggling with this problem for a few days now. Any help or suggestions at all would be really appreciated.

donalbain
  • 1,158
  • 15
  • 31

2 Answers2

0

Table views cache their cells to allow you to reuse them, which you're doing whenever you call dequeueReusableCellWithIdentifier:. You should call setSelected: before the end of your tableView:cellForRowAtIndexPath: method to synchronize the state of the button with the state of the Person instance that corresponds to the current row.

Another thing to consider is that creating new button instances each time you return a cell is pretty wasteful. Consider creating and configuring the buttons (setting titles, images, etc.) once per cell instance inside the if block where you're loading the cell from the nib file.

jlehr
  • 15,557
  • 5
  • 43
  • 45
  • I agree with your suggestion to update the state of the button as well after changing its image, and updated the code to do that. Unfortunately the problem remains. I also agree with your performance suggestion, but right now that's not really a main concern for me. Any other ideas about what could be causing this? I've also tried doing [tableViewCell.filterButton setNeedsDisplay] to no avail. – donalbain Feb 08 '11 at 20:25
  • Make sure you modify the code in your `updateViews` method to remove any calls the modify the buttons. In fact, you might want to change the name of that method, because it should only be updating the model, not the views. – jlehr Feb 08 '11 at 20:43
  • By the way, my personal take on this is that the use of button in this scenario just adds unnecessary complexity. Consider just changing the cell's accessory type to `UITableViewCellAccessoryCheckmark` to indicate whether a person is filtered (whatever that means; not clear on that), and implementing `tableView:didSelectRowAtIndexPath:` to respond to the user selecting the row. That's how table views are designed to work. – jlehr Feb 08 '11 at 20:49
  • Thanks a ton guys. I was not aware of this golden rule :P. So indeed the issue was with changing the structure in cellForRowAtIndex path. Setting the button's state alone does the trick. @jlehr I agree that I should remove the view updates to my button in the updateViews method and also that its pretty hacked together right now. I think it's best to set the button's image for state in the nib, and just change the state in my method. – donalbain Feb 08 '11 at 20:55
  • @jlehr I thought about doing this too, but I'm still not very familiar with the accessory view and how to customize it. My table cells have a fair bit of content in them and I'd rather have the user click on a button in the cell rather then the cell itself to do the check as this seems more intuitive to me. (The filterbutton in this case has an open/closed eye image). By the way, filtering in this case means to show/not show that particular person's list of 'Site' objects (which implement the annotation protocol) in my mapview. – donalbain Feb 08 '11 at 21:01
  • Setting the cell's accessoryType works like this: `[cell setAccessoryType:UITableViewCellAccessoryCheckmark];` -- that's all there is to it. To switch it back: `[cell setAccessoryType:UITableViewCellAccessoryNone];` – jlehr Feb 08 '11 at 21:24
0

This is a typical issue that happens when you change the cell view structure after it has been dequeued, while you're allowed to change the cell structure only in the alloc/init stage. After dequeue or alloc/init you are allowed to customize the content only and not the structure.

In your case, when the cell (let's say it is row-0) is loaded from the nib, the internal subviews structure is created (as defined in the Nib) and filterButton instance is assigned to one of these subviews. But a few lines below you create a new UIButton and replace the filterButton instance with this new one, but the real button subview will remain the same! Now when you click a button, of course the "real" button (that is the button in the cell view hierarchy which has been originally created by the Nib) will be triggered, the callback called and the state changed.

Later, when you scroll up this row-0 cell, it is removed from screen and enqued and then re-used for another cell, let's say row-9. At this point, former cell row-0 is going to be reused for cell row-9, but setting filterButton has still no effect, as you're keep going using the original button loaded initially by the Nib for cell row-0. Of course you will see these buttons states to be messed during scrollings as they are reused by the queue mechanism differently each time (so row-0 --> row-9, later row-0 --> row-8 and so on).

The solution is simply to change the button status: [self.filterButton setSelected:NO|YES] and not change the cell view content.

So the golden rule is: NEVER change the cell structure after you've dequed it. If you need to change the structure then use DIFFERENT cell IDs. Of course the more customizable is the cell the easier is the possibility to reuse them.

viggio24
  • 12,316
  • 5
  • 41
  • 34