Search code examples
iosxcodeuitableviewpushviewcontroller

Trouble with Cells in UITableView


I'm writing a simple checklist app. I have two UIViewControllers. The first displays the checklist in a UITableView. I'm using a UIBarButtonItem to push a second view onto the stack to add new tasks. All of the tasks are saved in an array.

Everything works great save for one thing.

If I enter edit mode and delete an item from the table view, the item is removed from the table view and the array--this part seems to be working fine. However, after deleting an item, if I tap the bar button item to add a new task, I run into a problem.

My NSLogs tell me that the new item is added to the array, but when I return to the table view, the deleted item shows up instead of the new item. The table view seems to be reusing the dequeued cell (not sure).

What am I doing wrong?

CLCheckListViewController.m

#import "CLCheckListViewController.h"
#import "CLTaskFactory.h"
#import "CLTaskStore.h"
#import "CLAddTaskViewController.h"

@implementation CLCheckListViewController
{
    __weak IBOutlet UITableView *checkList;
}

- (id)init
{
    self = [super init];
    if (self) {
        // add five sample tasks
        CLTaskFactory *task1 = [[CLTaskFactory alloc] init];
        [task1 setTaskName:@"Task 1"];
        [task1 setDidComplete:NO];
        [[CLTaskStore sharedStore] addTask:task1];

        CLTaskFactory *task2 = [[CLTaskFactory alloc] init];
        [task2 setTaskName:@"Task 2"];
        [task2 setDidComplete:NO];
        [[CLTaskStore sharedStore] addTask:task2];

        CLTaskFactory *task3 = [[CLTaskFactory alloc] init];
        [task3 setTaskName:@"Task 3"];
        [task3 setDidComplete:NO];
        [[CLTaskStore sharedStore] addTask:task3];

        CLTaskFactory *task4 = [[CLTaskFactory alloc] init];
        [task4 setTaskName:@"Task 4"];
        [task4 setDidComplete:NO];
        [[CLTaskStore sharedStore] addTask:task4];

        CLTaskFactory *task5 = [[CLTaskFactory alloc] init];
        [task5 setTaskName:@"Task 5"];
        [task5 setDidComplete:NO];
        [[CLTaskStore sharedStore] addTask:task5];
    }
    return self;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [checkList reloadData];
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    // create edit button
    [[self navigationItem] setLeftBarButtonItem:[self editButtonItem]];

    // create title
    [[self navigationItem] setTitle:@"Checklist"];

    // create add guest button
    UIBarButtonItem *bbi = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(pushAddTask)];
    [[self navigationItem] setRightBarButtonItem:bbi];
}

- (void)pushAddTask
{
    CLAddTaskViewController *advk = [[CLAddTaskViewController alloc] init];
    [[self navigationController] pushViewController:advk animated:YES];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[[CLTaskStore sharedStore] allTasks] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [checkList dequeueReusableCellWithIdentifier:CellIdentifier];

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

        // put the tasks into the cell
        [[cell textLabel] setText:[NSString stringWithFormat:@"%@", [[[CLTaskStore sharedStore] allTasks] objectAtIndex:[indexPath row]]]];

        // put the checkbox into the cell's accessory view
        UIButton *checkBox = [UIButton buttonWithType:UIButtonTypeCustom];
        checkBox = [UIButton buttonWithType:UIButtonTypeCustom];
        [checkBox setImage:[UIImage imageNamed:@"checkbox.png"] forState:UIControlStateNormal];
        [checkBox setImage:[UIImage imageNamed:@"checkbox-checked.png"] forState:UIControlStateSelected];
        checkBox.frame = CGRectMake(0, 0, 30, 30);
        checkBox.userInteractionEnabled = YES;
        [checkBox addTarget:self action:@selector(didCheckTask:) forControlEvents:UIControlEventTouchDown];
        cell.accessoryView = checkBox;
    }
    return cell;
}

- (void)didCheckTask:(UIButton *)button
{
    CGPoint hitPoint = [button convertPoint:CGPointZero toView:checkList];
    hitIndex = [checkList indexPathForRowAtPoint:hitPoint];

    task = [[[CLTaskStore sharedStore] allTasks] objectAtIndex:[hitIndex row]];

    if (task.didComplete) {
        task.didComplete = NO;
    } else {
        task.didComplete = YES;
    }

    NSInteger taskCount = [[[CLTaskStore sharedStore] allTasks] count];
    for (int i = 0; i < taskCount; i++) {
        NSLog(@"%@, status: %@", [[[CLTaskStore sharedStore] allTasks] objectAtIndex:i], [[[[CLTaskStore sharedStore] allTasks] objectAtIndex:i] didComplete]?@"YES":@"NO");
    }

    // toggle checkbox
    button.selected = !button.selected;
}

- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
    [super setEditing:editing animated:animated];

    // set editing mode
    if (editing) {
        self.navigationItem.title = @"Edit Checklist";
        [checkList setEditing:YES];
    } else {
        self.navigationItem.title = @"Checklist";
        [checkList setEditing:NO];
    }
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
                                            forRowAtIndexPath:(NSIndexPath *)indexPath
{
    // remove task
    if (editingStyle == UITableViewCellEditingStyleDelete) {

        // remove task from CLTaskStore
        task = [[[CLTaskStore sharedStore] allTasks] objectAtIndex:[indexPath row]];
        [[CLTaskStore sharedStore] removeTask:task];

        // remove guest from table view
        [checkList deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];

        // reload table view
        //[checkList reloadData];
    }
}

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
    [[CLTaskStore sharedStore] moveTaskAtIndex:[sourceIndexPath row] toIndex:[destinationIndexPath row]];
}

@end

CLAddTaskViewController.m

#import "CLAddTaskViewController.h"
#import "CLTaskFactory.h"
#import "CLTaskStore.h"

@implementation CLAddTaskViewController

    - (void)viewDidLoad
    {
        [[self navigationItem] setTitle:@"Add Task"];
    }

    - (void)viewWillDisappear:(BOOL)animated
    {
        [super viewWillDisappear:animated];

        // clear first responder
        [[self view] endEditing:YES];

        // create new task
        CLTaskFactory *newTask = [[CLTaskFactory alloc] init];
        [newTask setTaskName:[newTaskName text]];

        // add new guest to RCGuestStore
        [[CLTaskStore sharedStore] addTask:newTask];
    }

    @end

CLAddTaskFactory.m

#import "CLTaskFactory.h"

@implementation CLTaskFactory

@synthesize taskName;

- (void)setDidComplete:(BOOL)dc
{
    didComplete = dc;
}

- (BOOL)didComplete
{
    return didComplete;
}

- (NSString *)description
{
    // override the description
    NSString *descriptionString = [[NSString alloc] initWithFormat:@"%@", taskName];
    return descriptionString;
}

@end

CLAddTaskStore.m

#import "CLTaskStore.h"
#import "CLTaskFactory.h"
#import "CLCheckListViewController.h"

@implementation CLTaskStore

+ (id)allocWithZone:(NSZone *)zone
{
    return [self sharedStore];
}

+ (CLTaskStore *)sharedStore
{
    static CLTaskStore *sharedStore = nil;
    if (!sharedStore) {
        sharedStore = [[super allocWithZone:nil] init];
    }
    return sharedStore;
}

- (id)init
{
    self = [super init];
    if (self) {
        allTasks = [[NSMutableArray alloc] init];
    }
    return self;
}

- (NSMutableArray *)allTasks
{
    return allTasks;
}

- (void)addTask:(CLTaskFactory *)task
{
    [allTasks addObject:task];
    NSLog(@"Task added: %@", task);
}

- (void)removeTask:(CLTaskFactory *)task
{
    // remove the item for the deleted row from the store
    [allTasks removeObjectIdenticalTo:task];

    NSInteger taskCount = [allTasks count];
    NSLog(@"Removed: %@, there are now %d remaining tasks, they are:", task, taskCount);
    for (int i = 0; i < taskCount; i++) {
        NSLog(@"%@, status: %@", [[[CLTaskStore sharedStore] allTasks] objectAtIndex:i], [[[[CLTaskStore sharedStore] allTasks] objectAtIndex:i] didComplete]?@"YES":@"NO");
    }
}

- (void)moveTaskAtIndex:(int)from toIndex:(int)to
{
    if (from == to) {
        return;
    }

    CLTaskFactory *task = [allTasks objectAtIndex:from];
    [allTasks removeObjectAtIndex:from];
    [allTasks insertObject:task atIndex:to];
}

@end

Thanks for your help!


Solution

  • The table view seems to be reusing the dequeued cell

    That's exactly what it does; it reuses cells whenever possible. If you had enough items to cause the table view to scroll, you'd also see the same problem.

    In -tableView:cellForRowAtIndexPath: you must always set up the cell to display the correct content for the given row index, regardless of whether dequeueReusableCellWithIdentifier: returns a cell or not.

    Basically:

    • If -dequeueReusableCellWithIdentifier: returns nil, create a new cell and add your checkbox button to it.

    • Then set the cell text and the button state on either this new cell or the cell returned from -dequeueReusableCellWithIdentifier: