一个iOS7的bug引发的关于cell的事件处理

中午的时候,测试人员上报了一个在iOS7的bug,如下:

enter image description here

可以看到,本来点击的是第二项的➕号,但是,却变成第一项增加了,在模拟器上跑了一次:

enter image description here

正常运行,没有任何问题,应该是系统的原因造成的。

刚好最近弄了部iphone4,跑着iOS7的系统,重新跑了一次,果然,bug重现了,看下我们的代码:

cell.changeCarNumView.incrementCallback = ^(PKYStepper *stepper, float newValue){
            @strongify(self);
            if (!self) return;

            AfternoonTeaShopCell *superCell = (AfternoonTeaShopCell *)stepper.superview.superview;
            NSIndexPath *indexPath = [self.kitchenDetailsView.tableView indexPathForCell:superCell];

            KitchenDetailsTodayMenuModel *model = self.todayMenuArray[indexPath.row];
            model.selectCount = newValue;

            [self.kitchenDetailsView.tableView reloadRowAtIndexPath:indexPath withRowAnimation:UITableViewRowAnimationNone];
        };  

这里,stepper是加在AfternoonTeaShopCellcontentView里面,所以这里可以使用stepper.superview.superview来获取cell,然后获取cell相对应的indexPath,改变数量,刷新当前行.

这代码在iOS8(包括iOS8)运行良好,但是在iOS7却出现了问题,加了个断点,当我们点击加号的时候:

enter image description here

enter image description here

What the fuck!!!在当初你告诉我,见过你父亲和你爷爷就可以娶到你,此时此刻,当我历尽千辛万苦,来到你爷爷面前,桃花依旧,人不再,你爷爷说要再见过你曾祖父才行。

enter image description here

从上面的截图可以看出,在iOS7上和iOS9上,UITableViewCell的层次已经发生了改变,在iOS7上,我们可以看到多了一层UITableViewCellScrollvView,所以在iOS7上我们需要使用三个superview来获取celliOS7 UITableViewCell的变化 也阐述了iOS6-iOS7UITableViewCell视图层次的改变,但是在iOS8之后又变回了,所以:

if (IOS_VERSION >= 7.0 && IOS_VERSION < 8.0) {
    AfternoonTeaShopCell *superCell = (AfternoonTeaShopCell *)stepper.superview.superview.superview;
}else{
    AfternoonTeaShopCell *superCell = (AfternoonTeaShopCell *)stepper.superview.superview;
}    

当然,我们也可以使用stackoverflow上的一种方法:

UIView *view = stepper;
while (view != nil && ![view isKindOfClass:[UITableViewCell class]]) {
                view = [view superview];
            }
AfternoonTeaShopCell *superCell = (AfternoonTeaShopCell *)view;  

总算解决了bug,但是一切的源头还是由于我换了另一种方法去操作,并且考虑不周。

给UITableViewCell的按钮添加事件有三种方法:

enter image description here

Tag方式

通过给button设置tag,一般设置为indexPath.row

[cell.delBtn addTarget:self action:@selector(delItem:) forControlEvents:UIControlEventTouchUpInside];
cell.delBtn.tag = indexPath.row;  

然后,在点击事件delItem中:

- (void)delItem:(UIButton *)btn{
    NSInteger index = btn.tag;//1
    [self.dataArray removeObjectAtIndex:index];
    [self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:index inSection:0]] withRowAnimation:UITableViewRowAnimationMiddle];
}  

通过btn.tag获取我们当前操作了第几行,然后remove数据源(dataArray)对应的数据,再使用deleteRowsAtIndexPathstableview添加一个删除动画,一切没看来是那么perfect,让我们看看效果,运行:

enter image description here

还好没抱太大希望,不然会得到生命不能承受之重的绝望,当我们点击第一个删除按钮的时候,一切都是正常的,当我们点击第二个删除按钮的时候,我们点击的是title->1这一行,但是却是把title->2删除了,当我们点击最后一行的时候,直接crash。

这里我们将delItem的代码修改一下,将:

[self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:index inSection:0]] withRowAnimation:UITableViewRowAnimationMiddle];  

改为:

[self.tableView reloadData];  

运行:

enter image description here

一切正常,就是没有了删除动画,聪明的你应该已经知道答案了吧。

当我们使用deleteRowsAtIndexPaths方法时:

  1. 首先删除了第0行,这时候之前的第一行(title->1)成为了现在的第0行
  2. 当我们点击第0行(title->1)这行的删除按钮时,这时候我们需要的是删除第0行的数据,但是要注意一点,我们在cellForRowAtIndexPathbutton 设置的tag就是当前的行数,而我们调用的deleteRowsAtIndexPaths方法是不会调cellForRowAtIndexPath的,所以,我们删除了(title->0)这行之后,我们当前的(title->1)的tag还是1,而非我们想要的0,所以,这里会把(title->1)下面那行删除掉
  3. 当我们点击最后一行的时候,crash,我们看一张图:

    enter image description here

    此时数据源(dataArray)的count是7,但是我们的index却是9,所以是由数组越界引起的crash.

当我们使用reloadData

因为每次删除操作之后都调用了cellForRowAtIndexPath,所以保证了button.tag的值是最新的,所以可以正常运行,但是对于删除的操作就显得突兀,没有过渡动画.

Cell.superview方式

使用这种方式,我们不需要给button设置tag,下面是代码:

- (void)delItem:(UIButton *)btn{

    UIView *view = btn;
    while (view != nil && ![view isKindOfClass:[UITableViewCell class]]) {
        view = [view superview];
    }

    Cell *cell = (Cell *)view;
    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];

    [self.dataArray removeObjectAtIndex:indexPath.row];
    [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationMiddle];  
}   

delegate方式

通过delegate的方式,将button的点击事件回传.

delegate:

@protocol CellDelegate <NSObject> 

- (void)delBtnClick:(DelegateCell *)cell;  

@end  

DelegateCell的删除按钮点击事件中:

- (IBAction)delClick:(UIButton *)sender {
    if ([self.delegate respondsToSelector:@selector(delBtnClick:)]) {
        [self.delegate delBtnClick:self];
    }  
}  

记得在cellForRowAtIndexPath 中设置:

cell.delegate = self;  

最后,在当前控制器实现delegatedelBtnClick方法:

- (void)delBtnClick:(DelegateCell *)cell{

    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];

    [self.dataArray removeObjectAtIndex:indexPath.row];
    [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationMiddle];
}  

这种方式和Cell.superview方式类似,都是为了获取当前的cell,然后获得indexPath,再删除。

好了,以上就是UITableViewCell的删除按钮处理事件的几种方式,推荐使用后两种方式来进行操作,第一种虽然代码比较少,但是很难在保证tag 正确性的前提下加入删除动画,当然你也可以在删除动画完成后再使用reloadData 刷新列表,但是这没必要不是吗?如果你对第一种有更好的方法解决请一定要告诉我,我还是刚起飞的菜鸟,很多事情站在我的角度会看不全。

本文的Demo,包含swift和objc两个版本