[iOS] 通过 cell 的闭包 block 删除列表 item 时容易忽视的潜在崩溃问题

背景

有些需求会在 UITableViewCell 或者 UICollectionViewCell 上有一个删除按钮,用于删除这个 cell,正常的思路是:数据与界面同步,即删除 cell 时同时删除数据源,如下:

// Cell 的声明
static NSString *const  kIdentifier = @"kIdentifier";

@interface SomeTableViewCell : UITableViewCell

@property (nonatomic, copy) void(^deleteBlock)(SomeTableViewCell *);

@end


// 有问题的Cell 的数据源方法
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    SomeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kIdentifier];
    
    __weak typeof(self) weakSelf = self;
    cell.deleteBlock = ^(SomeTableViewCell *theCell){
        [weakSelf.dataArray removeObjectAtIndex:indexPath.row];  // 移除数据源
        [weakSelf.tableView deleteRowsAtIndexPaths:@[indexPath] 
                  withRowAnimation:UITableViewRowAnimationLeft]; // 移除cell
    };
    
    
    return cell;
}

注意示例代码中的两处 weakSelf 的使用都是必要的,存在潜在的循环引用需要进行手动处理,详见前文。

然则,以上示例的处理方式存在潜在的崩溃和业务异常风险。通过 cell 的 block 所捕获的 indexPath,在未删除 cell 的情况下的确对应了 cell 所在的位置,而在按照上述方式删除一个 cell 后,其他 cell 所在的真实 indexPath 已经发生了变化,而 cell 的 block 所捕获的 block 却并未变化,那么进一步删除第二个 cell 时,问题就会显现,即删除第二个 cell 时删除的实际是删除第一个cell 之前所捕获的 indexPath,也就是说,此时第二个 cell 所捕获的 indexPath 已经不代表 这个 cell 。

解决方案

既然明确了问题的来源,那么处理方式也很清晰。思路不变,在实现细节上进行优化,根据 cell 本身获取 indexPath 进行 cell 的删除,而不能使用外部所捕获的 indexPath,代码如下:

// 合理的 Cell 数据源方法
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath_right:(NSIndexPath *)indexPath {
    SomeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kIdentifier];
    
    __weak typeof(self) weakSelf = self;
    cell.deleteBlock = ^(SomeTableViewCell *theCell){
        // 切记这里不能使用外部的 tableView 变量,因为 cell 是 tableView 的间接子视图
        // cell 因此被 tableView 强引用
        // 如果在 block 内直接使用 tableView,则 cell 强引用 tableView,造成循环引用
        // 当然也不能直接使用外部的 cell 变量(这个Xcode 会直接给出警告)
        NSIndexPath *rightIndexPath = [weakSelf.tableView indexPathForCell:theCell]; // 获取真实 indexPath
        [weakSelf.dataArray removeObjectAtIndex:rightIndexPath.row]; // 移除真实数据源
        [weakSelf.tableView deleteRowsAtIndexPaths:@[rightIndexPath]
                     withRowAnimation:UITableViewRowAnimationLeft]; // 移除真实 cell
    };
    
    
    return cell;
}

小结

PS:还需要注意的一点在于 直接 reloadData 是没有问题的,但是会失去一些删除的动画效果,也是不必要的性能损耗。
同样地,在新增一个 cell 时也需要注意。

加我微信沟通。


你可能感兴趣的:([iOS] 通过 cell 的闭包 block 删除列表 item 时容易忽视的潜在崩溃问题)