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

时间:2015-06-30 17:34:00

标签: ios objective-c uitableview

撕掉我的头发试图让它发挥作用。我想执行[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];

这样可行但是如果我收到推送通知(即收到新消息),删除应用程序时会崩溃并显示如下错误:

  

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

之前

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

 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
删除时的

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

6 个答案:

答案 0 :(得分: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;
}

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

添加消息

[self.messagesToInsert addObject:message];
[self updateTableIfPossible];

删除邮件

[self.messagesToDelete addObject:message];
[self updateTableIfPossible];

此代码的作用是确保数据源的稳定性。每当有更改时,您都需要将需要插入/删除的消息添加到数组中(messagesToDeletemessagesToDelete)。然后调用函数updateTableIfPossible,它将更新表视图的数据源(并将动画更改),前提是当前没有动画正在进行中。如果正在进行动画,则在此阶段将无效。

然而,因为我们已添加完成

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

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

这是更安全的数据源更新方式。如果它适合你,请告诉我。

答案 1 :(得分:2)

删除一行

**arrptr

删除部分

注意:如果您的TableView有多个部分,那么当部分只包含一行而不是删除行时,您必须删除整个部分

int

答案 2 :(得分: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];

在您的代码中有:

/*
    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];
*/

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

deleteRowsAtIndexPaths:

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

假设/* ... [self.tableView reloadData]; [self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft]; This will cause a crash... */ 等于nil;

self.messageToDelete

我的建议是先检查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 `

self.messageToDelete

希望这很有帮助,干杯! ;)

答案 3 :(得分:1)

'尝试从更新'

之前仅包含1行的第0部分删除第1行

这表示您的索引路径在删除期间引用了第1行。但是第1行不存在。仅存在第0行(如该部分,它们从零开始)。

那么你如何得到一个大于元素数的indexPath?

我们可以看到插入行的通知处理程序吗?你可以做一些黑客攻击,如果动画正在进行中,你会在通知处理期间执行Selector:withDelay:以留出时间让动画完成。

答案 4 :(得分:1)

在您的代码中,崩溃的原因是:您正在更新数组但不更新UITableView数据源。因此,您的dataSource还需要在调用endUpdates时反映更改。

根据Apple文档。

ManageInsertDeleteRow

要为行和节的批量插入,删除和重新加载设置动画,请在连续调用beginUpdates和endUpdates定义的动画块中调用相应的方法。如果不在此块中调用插入,删除和重新加载方法,则行和节索引可能无效。可以嵌套对beginUpdates和endUpdates的调用;所有索引都被视为只有外部更新块。

在一个块结束时 - 也就是说,在endUpdates返回之后 - 表视图查询其数据源并像往常一样委托行和段数据。因此,应更新支持表视图的集合对象以反映新的或删除的行或节。

示例

    [tableView beginUpdates];
    [tableView deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
    [tableView endUpdates];

希望这对你有所帮助。

答案 5 :(得分:0)

我认为这里的许多评论都提到了答案,但我会尝试梳理一下。

第一个问题是,您似乎没有在beginUpdates / endUpdates方法中包含删除方法调用。这是我要解决的第一件事,否则在Apple的文档中警告说事情可能会出错。 (这并不意味着他们会出错,你这样做会更安全。)

完成此操作后,重要的是调用endUpdates检查数据源,以确保通过调用numberOfRows来计算任何插入或删除。例如,如果您删除一行(正如您所做),当您致电endUpdates时,最好确保您还从数据源中删除了某个项目。看起来你正在做那个部分,因为你要删除self.messages中的一个项目,我认为这是你的数据源。

(顺便提一下,你自己调用deleteRows...而不在beginUpdates / endUpdates包围,也可以在数据源上调用numberOfRows,你可以轻松查看在其中加入一个断点。但是,请不要这样做,始终使用beginUpdates / endUpdates。)

现在 可能在调用deleteRows...endUpdates之间,其他人正在通过向其添加对象来修改self.messages数组,但我不是&# 39;不知道我是否真的相信这一点,因为它必须非常不幸地计时。当您收到推送消息时,您不能正确处理行的插入,再次使用beginUpdates / endUpdates或执行其他操作时更有可能错误。如果您可以发布处理插入的代码部分,将会很有帮助。

如果该部分看起来没问题,那么看起来你很遗憾在调用删除代码时对另一个线程上的self.messages数组进行了更改。您可以通过添加日志行来检查此问题,其中插入代码会向数组添加消息:

NSLog(@"Running on %@ thread", [NSThread currentThread]);

或者只是在其中放置一个断点并查看最终结束的线程。如果这确实是问题,您可以通过执行以下操作来调度修改数组的代码并将行插入主线程(无论如何它应该是UI操作):

dispatch_async(dispatch_get_main_queue(), ^{
    [self.messages addObject:newMessage];
    [self.tableView beginUpdates];
    [self.tableView insertRows...]
    [self.tableView endUpdates];
});

这将确保在主线程忙于删除行时,消息数组不会被另一个线程变异。