Retaining UITableViewCell selection statuses

I’ve tried literally everything and anything I could think of. Can anyone figure out why the UITableViewCells don’t reload the checkmarks after selecting, scrolling away, then scrolling back?

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    #define CHECK_NULL_STRING(str) ([str isKindOfClass:[NSNull class]] || !str)?@"":str

    static NSString *CellIdentifier = @"inviteCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    cell.accessoryType = UITableViewCellAccessoryCheckmark;
    cell.textLabel.highlightedTextColor = [UIColor colorWithHexString:@"#669900"];
    cell.selectionStyle = UITableViewCellSelectionStyleGray;
    cell.backgroundColor = [UIColor blackColor];
    cell.textLabel.textColor = [UIColor whiteColor];
    [[UITableViewCell appearance] setTintColor:[UIColor colorWithHexString:@"#669900"]];

    if (cell == nil) {
        cell = [[UITableViewCell alloc] init];
    }

    if (cell == nil) {cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; }

    BOOL isSearching = tableView != self.tableView;
    NSArray *arrayToUse = (isSearching ? searchResults : contactsObjects);
    id p = arrayToUse[indexPath.row];

    NSString *fName = (__bridge_transfer NSString *)(ABRecordCopyValue((__bridge ABRecordRef)(p), kABPersonSortByFirstName));
    NSString *lName = (__bridge_transfer NSString *)(ABRecordCopyValue((__bridge ABRecordRef)(p), kABPersonSortByLastName));
    cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", CHECK_NULL_STRING(fName), CHECK_NULL_STRING(lName)];

    BOOL showCheckmark = [[stateArray objectAtIndex:indexPath.row] boolValue];
    if (showCheckmark == YES)
    {
        cell.accessoryType = UITableViewCellAccessoryCheckmark;
        NSLog(@"It hit showCheckmark = YES, and stateArray is %@",stateArray[indexPath.row]);
    }
    else
    {
        cell.accessoryType = UITableViewCellAccessoryNone;
        NSLog(@"It hit showCheckmark = NO, and stateArray is %@",stateArray[indexPath.row]);
    }

    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
{
    id object = contactsObjects[indexPath.row];
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];

    if (cell.accessoryType == UITableViewCellAccessoryNone)
    {
        cell.accessoryType = UITableViewCellAccessoryCheckmark;
        [stateArray insertObject:[NSNumber numberWithBool:YES] atIndex:indexPath.row];
        [selectedObjects addObject:object];
    }
    else
    {
        cell.accessoryType = UITableViewCellAccessoryNone;
        [stateArray insertObject:[NSNumber numberWithBool:NO] atIndex:indexPath.row];
        [selectedObjects removeObject:object];
    }

    //slow-motion selection animation.
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

  • PushPlugin not found, or is not a CDVPlugin
  • UITableViewCell ImageView not scaling properly?
  • How to set tcp_nodelay in GCDAsyncsocket?
  • GPUImage2 : HarrisCornerDetector not calling callback
  • Does Key Value Observing Work on UITextView's Text Property?
  • Images.xcassets bug upon Archive or Build for running
  • 4 Solutions Collect From Internet About “Retaining UITableViewCell selection statuses”

    You missed out the ! (inverse operator) on the following line meaning that the state will always be the same.

    [stateArray replaceObjectAtIndex:indexPath.row withObject:[NSNumber numberWithBool:[[stateArray objectAtIndex:indexPath.row] boolValue]]];
    

    It should be

    [stateArray replaceObjectAtIndex:indexPath.row withObject:[NSNumber numberWithBool:![[stateArray objectAtIndex:indexPath.row] boolValue]]];
    

    Edit — I’ve refactored both methods because it can be done with a lot less code and it will completely simplify the methods for you.

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        static NSString *CellIdentifier = @"inviteCell";
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    
        BOOL isSearching = tableView != self.tableView;
        NSArray *arrayToUse = (isSearching ? searchResults : contactsObjects);
        id p = arrayToUse[indexPath.row];
    
        NSString *fName = (__bridge_transfer NSString *)(ABRecordCopyValue((__bridge ABRecordRef)(p), kABPersonSortByFirstName));
        NSString *lName = (__bridge_transfer NSString *)(ABRecordCopyValue((__bridge ABRecordRef)(p), kABPersonSortByLastName));
        cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", CHECK_NULL_STRING(fName), CHECK_NULL_STRING(lName)];
    
        BOOL showCheckmark = [stateArray[indexPath.row] boolValue];
        if (showCheckmark == YES) {
            cell.accessoryType = UITableViewCellAccessoryCheckmark;
        }
        else {
            cell.accessoryType = UITableViewCellAccessoryNone;
        }
        return cell;
    }
    
    - (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
        id object = contactsObjects[indexPath.row];
        UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
        if (cell.accessoryType == UITableViewCellAccessoryNone) {
            cell.accessoryType = UITableViewCellAccessoryCheckmark;
            [selectedObjects addObject:object];
        }
        else {
            cell.accessoryType = UITableViewCellAccessoryNone;
            [selectedObjects removeObject:object];
        }
        stateArray[indexPath.row] = @(cell.accessoryType == UITableViewCellAccessoryCheckmark);
    }
    

    I suggest a more object oriented approach. This will ensure that your code is flexible and displays correctly all the time.

    For each item you wish to display in your table, have a corresponding object. You mentioned that you are displaying contacts, so let’s suppose your object is called “Contact”:

    //Contact.h
    
    @interface Contact : NSObject
    
    @property BOOL selected;
    @property NSString *name;
    
    @end
    
    //Contact.m
    #import Contact.h
    @implementation Contact
    
    + (id) contactWithName:(NSString*)name {
        Contact *nContact = [Contact new];
        nContact.name = name;
        nContact.selected = NO;
        return nContact;
    }
    @end
    

    Then, just make your view work something like this:

    //ContactView.m
    
    @interface ContactView()
    
    @property NSMutableArray *contacts;
    
    @end
    
    @implementation ContactView
    @synthesize contacts;
    
    - (void) viewDidLoad {
        [super viewDidLoad];
        //get your contact list here. When creating contacts, be sure to assign their selected and their name as you require.
        contacts = @[[Contact contactWithName:@"John"], [Contact contactWithName:@"Jane"]];
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        static NSString *cellID = @"inviteCell";
        UITableViewCell *cell = [tableview dequeueReusableCellWithIdentifier:cellID];
        if (cell == nil) cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
    
        Contact *cellContact = [contacts objectAtIndex:indexPath.row];
        cell.textLabel.text = cellContact.name;
        cell.accessoryType = cellContact.selected == YES ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
    
        return cell;
    }
    
    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
        Contact *cellContact = [contacts objectAtIndex:indexPath.row];
        cellContact.selected = !cellContact.selected;
        [contacts replaceObjectAtIndex:indexPath.row withObject:cellContact];
        [tableView reloadData]; //to refresh without animation
        //[tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [tableView numberOfSections])] withRowAnimation:UITableViewRowAnimationTop]; //to refresh with animation
    }
    
    @end
    

    And boom, easy to use tables that always look right, queue properly, and are object oriented for easy maintenance later.

    The cell selection problem was not solved, even when insertObject was replaced with replaceWithObject, however, one should not waste time setting BOOL objects with an NSInteger inside an NSMutableArray. Instead, for cell selection memory, one should use NSDictionary like this:

    @property (nonatomic, strong) NSMutableDictionary * selectedRowCollection;
    
    - (void)viewDidLoad{
    
        self.selectedRowCollection = [[NSMutableDictionary alloc] init];
    }
    
    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
    {    
        id object = contactsObjects[indexPath.row];
        UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    
        if (cell.accessoryType == UITableViewCellAccessoryNone)
        {
            cell.accessoryType = UITableViewCellAccessoryCheckmark;
            [self.selectedRowCollection setObject:@"1" forKey:[NSString stringWithFormat:@"%d",indexPath.row]]; 
        }
        else
        {
            cell.accessoryType = UITableViewCellAccessoryNone;
            [self.selectedRowCollection removeObjectForKey:[NSString stringWithFormat:@"%d",indexPath.row]];
        }
    
        //slow-motion selection animation.
        [tableView deselectRowAtIndexPath:indexPath animated:YES];
    }
    
       - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        BOOL showCheckmark =  [[self.selectedRowCollection valueForKey:[NSString stringWithFormat:@"%d",indexPath.row]] boolValue];
    
        if (showCheckmark == YES)
        {
            cell.accessoryType = UITableViewCellAccessoryCheckmark;
    
        }
        else
        {
            cell.accessoryType = UITableViewCellAccessoryNone;
        }
    }
    

    If this answer helped you at all, remember to bump this question up! Thanks!

    You are not doing this UITableViewCell *cell = [tableview dequeueReusableCellWithIdentifier:cellID];
    if (cell == nil) {cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
    }

    Allocating is necessary if cell is nil. Or it cause problem while scrolling.