在UITableViewCell的附件视图中使用自定义按钮时出现问题

时间:2012-05-07 03:36:44

标签: ios xcode uitableview uibutton accessoryview

我正在UITableView中构建一个简单的清单。我通过在导航栏中放置常用的编辑按钮来添加编辑功能。该按钮打开编辑模式。编辑模式非常有效,直到我在每个单元格的附件视图中添加自定义复选框(作为按钮)。我正在使用此代码执行此操作:

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

    UITableViewCell *cell = [tableView 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 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;

        // put the index path in the button's tag
        checkBox.tag = [indexPath row];
    }
    return cell;
}

如您所见,我正在使用按钮的标记将indexPath传递给我的didCheckTask:方法:

- (void)didCheckTask:(UIButton *)button
{
    task = [[[CLTaskStore sharedStore] allTasks] objectAtIndex:button.tag];
    task.didComplete = YES;

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

    [checkList reloadData];
}

复选框和编辑所有似乎都在表面上正常工作。但是,当我进入编辑模式,删除tableView中的项目然后尝试使用复选框时,会出现一个大问题。例如,如果我删除tableView中的第一项,然后尝试检查最后一项的复选框,则程序崩溃:

  

2012-05-06 21:45:40.645 CheckList [16022:f803] *终止app到期   未被捕获的异常'NSRangeException',原因:'* - [__ NSArrayM   objectAtIndex:]:索引4超出边界[0 .. 3]'

我一直试图找出这个bug的来源,但我没有运气。我真的可以使用一些帮助 - 我是可可的新手。相关代码如下。

CLTaskFactory.h

#import <Foundation/Foundation.h>

@interface CLTaskFactory : NSObject
{
    NSString *taskName;
    BOOL didComplete;
}

@property NSString *taskName;

- (void)setDidComplete:(BOOL)dc;
- (BOOL)didComplete;

@end

CLTaskFactory.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

CLTaskStore.h

#import <Foundation/Foundation.h>

@class CLTaskFactory;

@interface CLTaskStore : NSObject
{
    NSMutableArray *allTasks;
}

+ (CLTaskStore *)sharedStore;

- (NSMutableArray *)allTasks;
- (void)addTask:(CLTaskFactory *)task;
- (void)removeTask:(CLTaskFactory *)task;
- (void)moveTaskAtIndex:(int)from toIndex:(int)to;

@end

CLTaskStore.m

    #import "CLTaskStore.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];
    }

    - (void)removeTask:(CLTaskFactory *)task
    {
        [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(@"%@", [[[CLTaskStore sharedStore] allTasks] objectAtIndex:i]);
        }
    }

    - (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


CLChecklistViewController.h

    #import <Foundation/Foundation.h>

    @class CLTaskFactory;

    @interface CLCheckListViewController : UIViewController
    {
        CLTaskFactory *task;
    }

    - (void)didCheckTask:(UIButton *)button;

    @end

CLCheckListViewController.m

#import "CLCheckListViewController.h"
#import "CLTaskFactory.h"
#import "CLTaskStore.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)viewDidLoad
{
    [[self navigationItem] setTitle:@"Checklist"];

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

- (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 = [tableView 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 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;

        // put the index path in the button's tag
        checkBox.tag = [indexPath row];
    }
    return cell;
}

- (void)didCheckTask:(UIButton *)button
{
    task = [[[CLTaskStore sharedStore] allTasks] objectAtIndex:button.tag];
    task.didComplete = YES;

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

    [checkList reloadData];
}

- (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 guest from store
    if (editingStyle == UITableViewCellEditingStyleDelete) {

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

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

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

@end

非常感谢您的帮助和专业知识!

编辑:

我使用循环NSLog修改了两个方法以获得一些洞察力。首先,CLTaskStore:

- (void)removeTask:(CLTaskFactory *)task
{
    [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");
    }
}

其次,CLTaskListViewController:

- (void)didCheckTask:(UIButton *)button
{
    task = [[[CLTaskStore sharedStore] allTasks] objectAtIndex:button.tag];
    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;

    [checkList reloadData];
}

我发现了两件事。如果我向上删除,从下到上,则没有问题。我可以检查一切 - 一切正常。但是,如果我删除第一行然后检查最后一行程序崩溃。删除的NSLog很干净,工作正常。

如果我删除第一行并检查第四行,则检查来自CLTaskStore报告第5行的NSLog。

这就是问题所在。这两个在删除后肯定是不按顺序的。

2 个答案:

答案 0 :(得分:2)

你的整个问题源于使用标签指示按钮所在行的坏主意。当你没有从数据源中删除行时,这已经够糟了,但是当你这样做时,这就是你的问题可能会遇到。

使用表视图中点击项的位置,并从表视图中获取位置的索引路径,更加健壮,并且可以使用可编辑表和多节表。请参阅我的回答here中的示例代码。

如果你这样做,就不需要重新编制索引。

答案 1 :(得分:0)

在进入tableView的编辑模式后按下删除按钮时,必须从数据源中删除相应的数据项。您的代码显示您有一个removeTask:方法,但我没有看到您实际调用该方法从数据源中删除相应任务条目的位置。执行此操作的好地方是在视图控制器中的tableview:commitEditingStyle:forRowAtIndexPath:方法。

由于您要删除数据源中的相应项目,因此对代码的进一步研究表明您的复选框标记值仍具有其原始值。如果在最后一个之前删除任何tableView项,然后尝试检查最后一个,则didCheckTask方法尝试访问原始indexPath行值,该值现在不存在并导致边界异常。如果删除前两个单元格,则最后两个tableView项目都会导致异常,依此类推。

在didCheckTask方法中不起作用,但是在removeTask:方法中,从数据源中删除对象后,循环遍历其余对象并将每个标记设置为等于其对应的数组索引。在moveTaskAtIndex:toIndex:方法中,由于用户重新排序项而移动数组条目后,执行相同的操作 - 遍历数组并将每个标记设置为等于其在数组中的索引。