在 UITableViewCell 的附件视图中使用自定义按钮的问题
Posted
技术标签:
【中文标题】在 UITableViewCell 的附件视图中使用自定义按钮的问题【英文标题】:Problems using custom button in UITableViewCell's Accessory View 【发布时间】:2012-05-07 03:36:43 【问题描述】:我正在 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] * 终止应用程序到期 未捕获的异常 'NSRangeException',原因:'* -[__NSArrayM objectAtIndex:]: 索引 4 越界 [0 .. 3]'
我一直试图找出这个错误的来源,但我没有运气。我真的可以使用一些帮助 - 我是可可的新手。相关代码如下。
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
非常感谢您的帮助和专业知识!
编辑:
我使用循环 NSLogs 修改了两种方法以获得一些见解。一、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 的 NSLog 报告第 5 行已检查。
这就是问题所在。删除后两者肯定是乱序的。
【问题讨论】:
在didCheckTask方法中,能不能在方法的开头放一个NSLog,在[checkList reloadData]之前再放一个。并尝试再次生成错误。看看您是否可以看到在应用崩溃之前显示了哪个日志? 感谢您的帮助。我试过这个。第一个日志打印,第二个日志(在 reloadData 之前)没有打印。似乎索引是歪斜的,但我一生都无法看到它发生在哪里。在这种情况下使用按钮标签可靠吗? (即与编辑相结合)?? 看起来 allTasks 数组的项目比删除后的 tableview 少。您应该在删除后放置一些 NSLogs 来验证您的 allTasks 数组是否保留了正确的项目并与您当前的显示 tableview 一致。 我刚刚意识到在 cellForRowAtIndexpath 中,你确实有这行: [[cell textLabel] setText:[NSString stringWithFormat:@"%@", [[[CLTaskStore sharedStore] allTasks] objectAtIndex:[ indexPath 行]]]];所以,我之前的帖子可能不会实现! 【参考方案1】:您的整个问题源于使用标签来指示按钮所在行的坏主意。当您不从数据源中删除行时,这已经够糟糕了,但是当您删除时,这就是您遇到的问题可以碰到。
使用在表格视图中被点击的项目的位置,并从表格视图中获取位置的索引路径,更加健壮并且适用于可编辑表格和多节表格。请参阅我的答案here 中的示例代码。
如果您这样做,则无需重新编制索引。
【讨论】:
谢谢!我认为这可能是两天前的死胡同,但我被告知可以做到。作为菜鸟的一部分。我将使用您的建议重新编码。我会告诉你进展如何。 A+,非常好。非常感谢。【参考方案2】:在进入 tableView 的编辑模式后按下删除按钮时,必须从数据源中删除相应的数据项。您的代码显示您有一个 removeTask: 方法,但我看不到您实际调用该方法以从数据源中删除相应任务条目的位置。执行此操作的好地方是视图控制器中的 tableview:commitEditingStyle:forRowAtIndexPath: 方法。
由于您正在删除数据源中的相应项目,进一步研究代码表明您的复选框标签值仍然具有其原始值。如果您删除最后一个之前的任何 tableView 项目,然后尝试检查最后一个,您的 didCheckTask 方法会尝试访问原始 indexPath 行值,该值现在不存在并导致边界异常。如果删除前两个单元格,那么最后两个 tableView 项都会导致异常,以此类推。
在 didCheckTask 方法中不起作用,但在 removeTask: 方法中,从数据源中删除对象后,循环遍历剩余对象并将每个标记设置为其对应的数组索引。在 moveTaskAtIndex:toIndex: 方法中,在由于用户重新排序项目而移动数组条目后,执行相同的操作 - 循环遍历数组并将每个标记设置为其在数组中的索引。
【讨论】:
感谢您的帮助。但是我不确定我是否遵循。我的 CLChecklistViewController 既是我的代表又是数据源。当进入编辑模式时,消息 tableView:commitEditingStyle:forRowAtIndexPath: 被发送到数据源。在该方法中,我做了两件事:(1)从 CLTaskStore 单例中删除任务,(2)从表视图中删除任务。从删除按钮in 编辑模式调用该方法。请注意,删除在大多数情况下都按预期工作。它很接近,但并不完全正确。同样,当我删除第一项然后尝试检查最后一项时...问题。 抱歉,没有看到相关代码,所以不知道您是否从数据源中删除了记录。 不过,我认为您的问题源于您的复选框标签仍然具有其原始值的事实。因此,当您尝试检查最后一个复选框时,它曾经是表格视图中的第五个单元格,因此 indexPath 行值为 4。创建单元格时,您的复选框标记值设置为该值。删除该单元格之前的项目,除非您对复选框标签重新编号,否则它们将包含原始 indexPath 行值。该问题仅出现在表中的最后一项上,因为它是唯一一个其索引会导致边界违规的项。请参阅编辑后的答案。 是的,你完全正确。请参阅上面的我的编辑(看起来我正在你身后签到)。按钮的标签值必须减一(假设删除一次)。现在正在查看您的编辑...希望我可以修复我的标签... 嗯……有没有一种好的方法来协调索引?我正在尝试通过我的 CLTaskStore 重新索引 allTasks 数组以匹配 tableView:commitEditingStyle:forRowAtIndexPath 方法中的旧按钮标签,但到目前为止还没有乐趣。有没有更简单的方法来解决这个问题?以上是关于在 UITableViewCell 的附件视图中使用自定义按钮的问题的主要内容,如果未能解决你的问题,请参考以下文章
在 UITableViewCell 的突出显示上更改附件视图
从 UITableViewCell 的附件视图中删除视图不起作用