动态更改数据源导致deleteRowsAtIndexPaths:索引崩溃

Kex*_*Kex 17 objective-c uitableview ios

撕掉我的头发试图让它发挥作用.我想表演[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];,

我如何删除更详细的代码:

int index =  (int)[self.messages indexOfObject:self.messageToDelete];


[self.messages removeObject:self.messageToDelete];

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];

[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
Run Code Online (Sandbox Code Playgroud)

这工作正常但是如果我收到推送通知(即收到新消息),删除应用程序将崩溃并显示如下错误:

断言失败 - [UITableView _endCellAnimationsWithContext:],/ SourceCache/UIKit/UIKit-3347.44/UITableView.m:1327 2015-07-04 19:12:48.623 myapp [319:24083]***由于未捕获的异常而终止应用程序' NSInternalInconsistencyException',原因:'尝试从更新前仅包含1行的第0部分删除第1行'

我怀疑这是因为我的数据源正在改变,数组的大小

 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
Run Code Online (Sandbox Code Playgroud)

删除时的引用将不一致,因为当推送通知触发刷新时它会增加1.有什么方法可以解决这个问题吗?我是否正确deleteRowsAtIndexPaths使用该numberOfRowsInSection方法?

And*_*huk 5

因此,为了解决您的问题,您需要确保在放置一些表格视图动画时数据源不会更改。我建议执行以下操作。

首先,创建两个数组:messagesToDeletemessagesToInsert。这些将保存有关您要删除/插入哪些消息的信息。

其次,将布尔属性添加updatingTable到表视图数据源。

第三,添加以下功能:

-(void)updateTableIfPossible {
    if (!updatingTable) {
        updatingTable = [self updateTableViewWithNewUpdates];
    }
}

-(BOOL)updateTableViewWithNewUpdates {
     if ((messagesToDelete.count == 0)&&(messagesToInsert.count==0)) {
         return false;
     }
     NSMutableArray *indexPathsForMessagesThatNeedDelete = [[NSMutableArray alloc] init];
     NSMutableArray *indexPathsForMessagesThatNeedInsert = [[NSMutableArray alloc] init];
     // for deletion you need to use original messages to ensure
     // that you get correct index paths if there are multiple rows to delete
     NSMutableArray *oldMessages = [self.messages copy];
     for (id message in messagesToDelete) {
         int index =  (int)[self.oldMessages indexOfObject:message];
         [self.messages removeObject:message];
         NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
         [indexPathsForMessagesThatNeedDelete addObject:indexPath];
     }
     for (id message in messagesToInsert) {
         [self.messages insertObject:message atIndex:0];
         NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
         [indexPathsForMessagesThatNeedInsert addObject:indexPath];
     }
     [messagesToDelete removeAllObjects];
     [messagesToInsert removeAllObjects];

     // at this point your messages array contains
     // all messages which should be displayed at CURRENT time

     // now do the following

     [CATransaction begin];

     [CATransaction setCompletionBlock:^{
         updatingTable = NO;
         [self updateTableIfPossible];
     }];

    [tableView beginUpdates];

    [tableView deleteRowsAtIndexPaths:indexPathsForMessagesThatNeedDelete withRowAnimation:UITableViewRowAnimationLeft];
    [tableView insertRowsAtIndexPaths:indexPathsForMessagesThatNeedInsert withRowAnimation:UITableViewRowAnimationLeft];

    [tableView endUpdates];

    [CATransaction commit];
    return true;
}
Run Code Online (Sandbox Code Playgroud)

最后,您需要在要添加/删除行的所有函数中使用以下代码。

添加信息

[self.messagesToInsert addObject:message];
[self updateTableIfPossible];
Run Code Online (Sandbox Code Playgroud)

删除信息

[self.messagesToDelete addObject:message];
[self updateTableIfPossible];
Run Code Online (Sandbox Code Playgroud)

该代码的作用是确保数据源的稳定性。只要有更改,就将需要插入/删除的消息添加到数组(messagesToDeletemessagesToDelete)中。然后,您调用一个函数updateTableIfPossible,该函数将更新表视图的数据源(并为更改添加动画效果),前提是当前没有正在进行的动画。如果正在进行动画制作,则在此阶段将不执行任何操作。

但是,因为我们添加了完成

[CATransaction setCompletionBlock:^{
     updatingTable = NO;
     [self updateTableIfPossible];
}];
Run Code Online (Sandbox Code Playgroud)

在动画结束时,我们的数据源将检查是否需要将任何新更改应用于表格视图,如果有,则将更新动画。

这是更新数据源的更安全的方法。请让我知道它是否适合您。


0ye*_*eoj 1

我对上面的代码进行了一些测试。你不能同时做到这一点,我们至少可以做的是:

//This will wait for `deleteRowsAtIndexPaths:indexes` before the `setCompletionBlock `
//
[CATransaction begin];

    [CATransaction setCompletionBlock:^{
        [tableView reloadData];
    }];

    [tableView beginUpdates];

        [tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];

    [tableView endUpdates];

[CATransaction commit];
Run Code Online (Sandbox Code Playgroud)

在你的代码中有:

/*
    int index =  (int)[self.messages indexOfObject:self.messageToDelete];

    [self.messages removeObject:self.messageToDelete];

    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];

    NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];

    [self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
*/
Run Code Online (Sandbox Code Playgroud)

这是不安全的,因为如果触发通知并且由于某种原因在[self.tableView reloadData];通知之前调用deleteRowsAtIndexPaths将导致崩溃,因为 tableView 当前正在更新数据然后被 中断deleteRowsAtIndexPaths:,请尝试按以下顺序进行检查:

/*
    ...

    [self.tableView reloadData];

    [self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];

    This will cause a crash...
*/
Run Code Online (Sandbox Code Playgroud)

嗯.. 回到你的代码,让我们进行一个可能导致崩溃的模拟。这只是一个假设,所以这并不是 100% 确定 (99%)。:)

假设self.messageToDelete等于 nil;

int index =  (int)[self.messages indexOfObject: nil]; 
// since you casted this to (int) with would be 0, so `index = 0`

[self.messages removeObject: nil]; 
// self.messages will remove nil object which is non existing therefore
// self.messages is not updated/altered/changed

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
// indexPath.row == 0

NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];

[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
// then you attempted to delete index indexPath at row 0 in section 0
// but the datasource is not updated meaning this will crash.. 
//
// Same thing will happen if `self.messageToDelete` is not existing in your `self.messages `
Run Code Online (Sandbox Code Playgroud)

我的建议是检查第self.messageToDelete一个:

if ([self.messages containsObject:self.messageToDelete])
{
    // your delete code...
}
Run Code Online (Sandbox Code Playgroud)

希望这对您有所帮助,干杯!;)