美文网首页
-[UITableView _endCellAnimations

-[UITableView _endCellAnimations

作者: 充满活力的早晨 | 来源:发表于2019-07-05 18:05 被阅读0次
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-3347.44/UITableView.m:1607
libc++abi.dylib: terminate_handler unexpectedly threw an exception

今天做tableview编辑cell并对cell进行删除操作,发现了以上崩溃.因此,刚开始学ios的时候就出现过这个崩溃,当时解决的方式是全部cell 重新reload .但是导致的结果是没有删除cell的动画了.因此今天想好好解决下这个bug.

准备工程

创建工程修改ViewController源码为下

//
//  ViewController.m
//  TableviewDeleteCell
//
//  Created by glodon on 2019/7/5.
//  Copyright © 2019 glodon. All rights reserved.
//

#import "ViewController.h"
#import "TestTableViewCell.h"

@interface SectionModel : NSObject
@property (nonatomic ,strong) NSMutableArray * rows ;
@property (nonatomic ,assign) BOOL isDelete ;
@end
@implementation SectionModel


@end
@interface RowModel : NSObject
@property (nonatomic ,assign) BOOL isDelete ;
@end
@implementation RowModel

@end

@interface ViewController ()<UITableViewDelegate,UITableViewDataSource>
@property (nonatomic ,strong) UITableView * tableView ;
@property (nonatomic ,strong) NSMutableArray * dataSource ;
@end

@implementation ViewController



-(RowModel *)createRowModel{
    RowModel * model = [RowModel new];
    model.isDelete = NO;
    return model;
}

-(SectionModel *)createSectionModel{
    SectionModel * model = [SectionModel new];
    model.rows = [NSMutableArray array];
    for (int i=0; i<2; i++) {
        [model.rows addObject:[self createRowModel]];
    }
    return model;
}

-(void)setDataSource{
    self.dataSource = [NSMutableArray array];
    for (int i=0; i<10; i++) {
         [self.dataSource addObject:[self createSectionModel]];
    }
}

-(void)createOperationButton{
    UIButton * button = [[UIButton alloc]initWithFrame:CGRectMake(0, 0, 100, 64)];
    [self.view addSubview:button];;
    button.backgroundColor =[UIColor redColor];
    [button setTitle:@"编辑" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(edit) forControlEvents:UIControlEventTouchUpInside];
    
    button = [[UIButton alloc]initWithFrame:CGRectMake(200, 0, 100, 64)];
    [self.view addSubview:button];;
    button.backgroundColor =[UIColor redColor];
    [button setTitle:@"删除" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(delete) forControlEvents:UIControlEventTouchUpInside];
}

-(void)edit{
    if (self.tableView.editing) {
          self.tableView.editing = NO;
    }else{
          self.tableView.editing = YES;
    }
}

-(void)delete{
    
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setDataSource];
    [self createOperationButton];
    self.tableView = [[UITableView alloc]initWithFrame:CGRectMake(0, 64, UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height-64) style:UITableViewStyleGrouped];
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    [self.tableView registerClass:[TestTableViewCell class] forCellReuseIdentifier:@"cell"];
    [self.view addSubview:self.tableView];
    self.tableView.allowsMultipleSelectionDuringEditing= YES;
    
}
#pragma mark  - delegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return 44;
}

#pragma mark  - datasource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
  SectionModel *sec =  self.dataSource[section];
    return sec.rows.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    TestTableViewCell * cell = (TestTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"cell"];
    [cell updateIndex:indexPath];
    return cell;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return self.dataSource.count;
}

-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
    UILabel * label = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, UIScreen.mainScreen.bounds.size.width, 40)];
    label.text = [NSString stringWithFormat:@" head section %d",section];
    return label;
}

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section{
    return [UIView new];
}


-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
    return 40;
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{
    return 10;
}

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    if (self.tableView.editing==YES) {
            SectionModel *sec =  self.dataSource[indexPath.section];
            sec.isDelete = !sec.isDelete;
            RowModel *row =   sec.rows[indexPath.row];
            row.isDelete = !row.isDelete;
    }
}

@end

添加新的cell类

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface TestTableViewCell : UITableViewCell
-(void)updateIndex:(NSIndexPath *)indexPath;
@end

NS_ASSUME_NONNULL_END


#import "TestTableViewCell.h"
@interface TestTableViewCell()
@property (nonatomic ,strong) UILabel * indexCell ;
@end
@implementation TestTableViewCell

- (void)awakeFromNib {
    [super awakeFromNib];
    // Initialization code
}

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

    // Configure the view for the selected state
}
-(void)updateIndex:(NSIndexPath *)indexPath{
    [self.contentView addSubview:self.indexCell];
    self.indexCell.text =[NSString stringWithFormat:@"%@-%@",@(indexPath.section),@(indexPath.row)];
}

-(UILabel *)indexCell{
    if (!_indexCell) {
        _indexCell = [[UILabel alloc]initWithFrame:self.contentView.bounds];
    }
    return _indexCell;
}
@end

运行结果

工程很简单,就是实现多个section的tableview.并且开启了编辑模式

bug 探索

做动画删除需要成对使用

- (void)beginUpdates;
- (void)endUpdates;

ios 11 以后建议使用

- (void)performBatchUpdates:(void (NS_NOESCAPE ^ _Nullable)(void))updates completion:(void (^ _Nullable)(BOOL finished))completion API_AVAILABLE(ios(11.0), tvos(11.0));

因此这里最好对以上api进行简单的封装.(本文没有进行封装)

section 删除

- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;

删除section 但是不删除数据源

修改函数-(void)delete如下

-(void)delete{
    NSMutableIndexSet * set =[NSMutableIndexSet indexSet];
    for (NSInteger i = self.dataSource.count-1; i>=0; i--) {
        SectionModel * model = self.dataSource[I];
        if (model.isDelete) {
            [set addIndex:i];
//            [self.dataSource removeObjectAtIndex:i];
        }
    }
    
    [self.tableView beginUpdates];
    [self.tableView deleteSections:set withRowAnimation:0];
    [self.tableView endUpdates];
    
}

启动项目删除section 0

删除section 0
项目崩溃了. 崩溃信息如下
2019-07-05 16:25:40.210408+0800 TableviewDeleteCell[95112:8286660] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3698.103.12/UITableView.m:2043

2019-07-05 16:25:45.070585+0800 TableviewDeleteCell[95112:8286660] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections.  The number of sections contained in the table view after the update (10) must be equal to the number of sections contained in the table view before the update (10), plus or minus the number of sections inserted or deleted (0 inserted, 1 deleted).'
*** First throw call stack:
(
    0   CoreFoundation                      0x00000001087036fb __exceptionPreprocess + 331
    1   libobjc.A.dylib                     0x0000000107ca7ac5 objc_exception_throw + 48
    2   CoreFoundation                      0x0000000108703482 +[NSException raise:format:arguments:] + 98
    3   Foundation                          0x00000001076f5927 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 194
    4   UIKitCore                           0x000000010ceb5b8b -[UITableView _endCellAnimationsWithContext:] + 17759
    5   UIKitCore                           0x000000010cecee6f -[UITableView endUpdates] + 74
    6   TableviewDeleteCell                 0x00000001073cbe13 -[ViewController delete] + 515
    7   UIKitCore                           0x000000010ccb9204 -[UIApplication sendAction:to:from:forEvent:] + 83
    8   UIKitCore                           0x000000010c70ec19 -[UIControl sendAction:to:forEvent:] + 67
    9   UIKitCore                           0x000000010c70ef36 -[UIControl _sendActionsForEvents:withEvent:] + 450
    10  UIKitCore                           0x000000010c70deec -[UIControl touchesEnded:withEvent:] + 583
    11  UIKitCore                           0x000000010ccf1eee -[UIWindow _sendTouchesForEvent:] + 2547
    12  UIKitCore                           0x000000010ccf35d2 -[UIWindow sendEvent:] + 4079
    13  UIKitCore                           0x000000010ccd1d16 -[UIApplication sendEvent:] + 356
    14  UIKitCore                           0x000000010cda2293 __dispatchPreprocessedEventFromEventQueue + 3232
    15  UIKitCore                           0x000000010cda4bb9 __handleEventQueueInternal + 5911
    16  CoreFoundation                      0x000000010866abe1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    17  CoreFoundation                      0x000000010866a463 __CFRunLoopDoSources0 + 243
    18  CoreFoundation                      0x0000000108664b1f __CFRunLoopRun + 1231
    19  CoreFoundation                      0x0000000108664302 CFRunLoopRunSpecific + 626
    20  GraphicsServices                    0x0000000110cc02fe GSEventRunModal + 65
    21  UIKitCore                           0x000000010ccb7ba2 UIApplicationMain + 140
    22  TableviewDeleteCell                 0x00000001073cd110 main + 112
    23  libdyld.dylib                       0x0000000109fe2541 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

从上面的崩溃信息我们能看出来,数据源 刚开始的时候是返回的数据条数是10 .当我们点击删除的操作的时候,我们没有更新数据源,那么数据源返回的条数还是10.但是苹果sdk内部计算条数应该是显示10-1=9 .与现在返回的条数不一致.因此报错.

修改源代码如下

static int sectionNum= 0;
-(void)delete{
    NSMutableIndexSet * set =[NSMutableIndexSet indexSet];
    for (NSInteger i = self.dataSource.count-1; i>=0; i--) {
        SectionModel * model = self.dataSource[I];
        if (model.isDelete) {
            [set addIndex:i];
//            [self.dataSource removeObjectAtIndex:i];
        }
    }
    sectionNum = 9;
    [self.tableView beginUpdates];
    [self.tableView deleteSections:set withRowAnimation:0];
    [self.tableView endUpdates];
    
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return sectionNum;
}

我们发现删除一条数据是不发生崩溃 的.但是删除两条还是会崩溃.

因此,这里我们在删除section的数据的时候,我们必须保证- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 返回的数据也要删除相应的条数

删除section 同时删除数据源

修改-(void)delete如下

-(void)delete{
    NSMutableIndexSet * set =[NSMutableIndexSet indexSet];
    for (NSInteger i = self.dataSource.count-1; i>=0; i--) {
        SectionModel * model = self.dataSource[I];
        if (model.isDelete) {
            [set addIndex:i];
            [self.dataSource removeObjectAtIndex:i];
        }
    }
    
    [self.tableView beginUpdates];
    [self.tableView deleteSections:set withRowAnimation:0];
    [self.tableView endUpdates];
    
}

我们发现,程序不会发生崩溃.以为删除的section 和 返回的- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 的数据是一致的.

其实删除section 一般情况下不会发生错误的.只要保证删除section的同时也要删除数据源就可以了

row 删除

- (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;

删除row的时候,发生崩溃的情况比较多.
这里先要贴张图多个section的.tableview的数据结构


删除row 和数据源不同步

这种情况和删除section是一样的结果的

-(void)delete{
    NSMutableArray * array =[NSMutableArray array];
    for (NSInteger i = self.dataSource.count-1; i>=0; i--) {
        SectionModel * model = self.dataSource[I];
       
        for (NSInteger j=model.rows.count-1; j>=0; j--) {
            RowModel * row = model.rows[j];
            if (row.isDelete) {
                [array addObject:[NSIndexPath indexPathForRow:j inSection:i]];
            }
        }
    }
    
    [self.tableView beginUpdates];
    [self.tableView deleteRowsAtIndexPaths:array withRowAnimation:0];
    [self.tableView endUpdates];
}

这是因为- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 函数返回的条数和系统计算后的条数不一致导致的

当section中的数据是多条,删除section中row 保证section的数据源至少有一条
-(void)delete{
    NSMutableArray * array =[NSMutableArray array];
    for (NSInteger i = self.dataSource.count-1; i>=0; i--) {
        SectionModel * model = self.dataSource[I];
       
        for (NSInteger j=model.rows.count-1; j>=0; j--) {
            RowModel * row = model.rows[j];
            if (row.isDelete) {
                [model.rows removeObjectAtIndex:j];
                [array addObject:[NSIndexPath indexPathForRow:j inSection:i]];
            }
        }
    }
    
    [self.tableView beginUpdates];
    [self.tableView deleteRowsAtIndexPaths:array withRowAnimation:0];
    [self.tableView endUpdates];
}

这种情况其实不会发生崩溃现象.

当section中的数据是多条,删除section中row 使section中的数据条数是0 .

测试代码同上,这个时候我们发现项目也没有发生崩溃现象


这说明,我们调用- (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;只要保证删除的row 和数据源对应起来就可以了.但是不能改变原来section的数量.

这里需要特别注意,当section的row 的数量是0的时候,千万别把section 给删除了. 比如 数据源 @[@[0,1,2,3,],@[1] ] 删除section= 1 row =0 的row. 最终数据源应该是@[@[0,1,2,3,],@[] ] 而不是 @[@[0,1,2,3,]] (改变section的数量了)

同时删除section 和 row

这种情况是很常见了.也是我们经常出错的地方.
当我们数据源删除了section 和row的时候,往往在调用只调用了- (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;而没有调用- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; .
这主要是数据源和删除数据不同步导致的
例如下面错误代码(数据源不同步)


-(void)delete{
    NSMutableArray * array =[NSMutableArray array];
    for (NSInteger i = self.dataSource.count-1; i>=0; i--) {
        SectionModel * model = self.dataSource[I];
       
        for (NSInteger j=model.rows.count-1; j>=0; j--) {
            RowModel * row = model.rows[j];
            if (row.isDelete) {
                [model.rows removeObjectAtIndex:j];
                [array addObject:[NSIndexPath indexPathForRow:j inSection:i]];
            }
            if (model.rows.count==0) {
                [self.dataSource removeObjectAtIndex:i];
            }
        }
    }
    
    [self.tableView beginUpdates];
    [self.tableView deleteRowsAtIndexPaths:array withRowAnimation:0];
    [self.tableView endUpdates];
    
}

产生崩溃

2019-07-05 17:55:59.705689+0800 TableviewDeleteCell[97755:8372592] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3698.103.12/UITableView.m:2043
2019-07-05 17:55:59.711038+0800 TableviewDeleteCell[97755:8372592] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections.  The number of sections contained in the table view after the update (9) must be equal to the number of sections contained in the table view before the update (10), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).'

修正保证数据源同步就行了

-(void)delete{
    NSMutableArray * array =[NSMutableArray array];
    NSMutableIndexSet * set =[NSMutableIndexSet indexSet];
    for (NSInteger i = self.dataSource.count-1; i>=0; i--) {
        SectionModel * model = self.dataSource[i];
       
        for (NSInteger j=model.rows.count-1; j>=0; j--) {
            RowModel * row = model.rows[j];
            if (row.isDelete) {
                [model.rows removeObjectAtIndex:j];
                [array addObject:[NSIndexPath indexPathForRow:j inSection:i]];
            }
            if (model.rows.count==0) {
                [self.dataSource removeObjectAtIndex:i];
                [set addIndex:i];
            }
        }
    }
    
    [self.tableView beginUpdates];
    [self.tableView deleteSections:set withRowAnimation:0];
    [self.tableView deleteRowsAtIndexPaths:array withRowAnimation:0];
    [self.tableView endUpdates];
    
}

总结

对应关系
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; 删除的section个数必须等于原来的数据源个数-现在数据源section个数
- (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; 删除的section中的 row 个数 必须等于原来section中row的个数-现在section中row的个数

相关文章

网友评论

      本文标题:-[UITableView _endCellAnimations

      本文链接:https://www.haomeiwen.com/subject/zfkehctx.html