如何在UITableView中正确处理可折叠部分更新(在调用-controllerDidChangeContent期间严重应用程序更新)

时间:2014-04-28 18:02:11

标签: ios uitableview core-data

我正在研究一个涉及可折叠部分的核心数据应用程序。

基本上,如果我跨部门移动托管对象,或者只是在其部分打开时删除它,我会收到以下错误:

  

2014-04-28 19:38:44.690 uRSS [663:60b] CoreData:错误:严重的应用程序错误。在调用-controllerDidChangeContent:期间,从NSFetchedResultsController的委托中捕获到异常。更新无效:第7节中的行数无效。更新后的现有部分中包含的行数(2)必须等于更新前该部分中包含的行数(3),加上或减去数字从该部分插入或删除的行(0插入,0删除)和加或减移入或移出该部分的行数(0移入,0移出)。 with userInfo(null)

我相信我处理didChange Object方法变化的方式,但我遇到了ATM:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;

    switch(type) {
        case NSFetchedResultsChangeInsert:
            NSLog(@"MasterViewController::didChangeObject **** Inserting something in %@**** ", _detailViewController.detailItem.category.name);
            if ([_detailViewController.detailItem.category.open boolValue]) {
                [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
            break;

        case NSFetchedResultsChangeDelete:
            NSLog(@"MasterViewController::didChangeObject **** Deleting something in %@**** ", _detailViewController.detailCategory.name);

            if ([_detailViewController.detailCategory.open boolValue]) {
                [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
        break;

        case NSFetchedResultsChangeUpdate:
            NSLog(@"MasterViewController::didChangeObject **** Changing something in %@**** ", _detailViewController.detailCategory.name);

            if ([_detailViewController.detailCategory.open boolValue]) {
                [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            }
            break;

        case NSFetchedResultsChangeMove:
            NSLog(@"MasterViewController::didChangeObject **** Moving something from %@ to %@**** ", _detailViewController.detailCategory.name, _detailViewController.detailItem.category.name);

            if ([_detailViewController.detailCategory.open boolValue]) {
                [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
            if ([_detailViewController.detailItem.category.open boolValue]) {
                [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
            break;
    }
}

我的detailView附加了2个对象:detailItem,一个Feed,detailCategory,原始类别,如果它发生变化。

类别有nameopen属性。如果open为false,则类别(部分)将折叠。

到目前为止,我无法在没有崩溃的情况下删除对象或更改其类别。 如果该类别已关闭,我可以将其删除。

每当创建一个新对象(具有自己的“新”类别)时,它就会错误地出现在当前打开的类别中。如果没有打开任何类别,那么它将使用新对象创建新类别。

有人可以告诉我如何处理这个问题吗?

UPDATE(1):以下是UIDataSource的更多信息:

这是我在详细信息视图中移动Feed Categories的时间和方式:

-(void) updateCategory:(id)sender
{
    if (!_categoriesPopover) {
        return;
    }
    [_categoriesPopover dismissPopoverAnimated:YES];
    [_mainMOC performBlockAndWait:^{
        Category *category=_categoryView.categoryChosen;
        if (!category) {
            return;
        }

        self.detailItem.category.open=[NSNumber numberWithBool:NO];

        self.detailCategory = self.detailItem.category;
        self.detailItem.category=category;

        self.detailItem.category.open=[NSNumber numberWithBool:YES];

        NSError *error;
        if (![_mainMOC save:&error])
        {
          NSLog(@"DetailViewController::updateCategory Error saving context: %@", error);
        } else {
          [[NSNotificationCenter defaultCenter] postNotificationName:@"feedUpdatedInDetailView" object:nil];
        }
    }];
}

一旦我在此屏幕截图中点击了所选目的地Category,就会发生这种情况: My RSS Reader Interface during the Category switch operation. Change Category is the folder item in the bottom menu bar of the detail View

UPDATE(2):

以下是numberOfRowsInSection方法:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
    return [_filteredCategoryArray count];
} else {
    if ([[[self.fetchedResultsController sections][section] objects] count]>0) {
        Category *cat = ((Feed *)[[[self.fetchedResultsController sections][section] objects] objectAtIndex:0]).category;
        return [cat.open boolValue] ? [[self.fetchedResultsController sections][section] numberOfObjects] : 0;
    } else {
        return 0;
    }
}
}

UPDATE(3):

我NSLog了一些变量,以便了解发生了什么,我想我们可以忘记这里的开/关方面。

a)以下是DetailView中发生的事情:

-(void) updateCategory:(id)sender
{
if (!_categoriesPopover) {
    return;
}
[_categoriesPopover dismissPopoverAnimated:YES];
Category *category=_categoryView.categoryChosen;
if (!category) {
    return;
}

[_mainMOC performBlockAndWait:^{
    NSLog(@"BEFORE: oldcat: %@ (%d) / newcat: %@ (%d)", _detailItem.category.name, [_detailItem.category.feeds count], category.name, [category.feeds count]);
    self.detailItem.category.open=[NSNumber numberWithBool:NO];

    self.detailCategory = self.detailItem.category;
    self.detailItem.category=category;

    self.detailItem.category.open=[NSNumber numberWithBool:YES];

    NSError *error;
    if (![_mainMOC save:&error])
    {
        NSLog(@"DetailViewController::updateCategory Error saving context: %@", error);
    } else {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"feedUpdatedInDetailView" object:nil];
    }
    NSLog(@"AFTER: oldcat: %@ (%d) / newcat: %@ (%d)", _detailCategory.name, [_detailCategory.feeds count], _detailItem.category.name, [_detailItem.category.feeds count]);
}];
}

b)以下是MasterView中的内容:

            case NSFetchedResultsChangeMove:
            NSLog(@"MVC: oldcat: %@ (%d) / newcat: %@ (%d)", _detailViewController.detailCategory.name, [_detailViewController.detailCategory.feeds count], _detailViewController.detailItem.category.name, [_detailViewController.detailItem.category.feeds count]);

            NSLog(@"MasterViewController::didChangeObject **** Moving something from %@ to %@**** ", _detailViewController.detailCategory.name, _detailViewController.detailItem.category.name);

            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

c)以下是日志的内容:

2014-04-30 18:52:04.771 uRSS[465:60b] BEFORE: oldcat: trucs (2) / newcat: Misc (7)
2014-04-30 18:52:04.771 uRSS[465:60b] MVC: oldcat: trucs (1) / newcat: Misc (8)
2014-04-30 18:52:04.772 uRSS[465:60b] MasterViewController::didChangeObject **** Moving something from trucs to Misc**** 
2014-04-30 18:52:04.772 uRSS[465:60b] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2935.137/UITableView.m:1368
2014-04-30 18:52:04.772 uRSS[465:60b] CoreData: error: Serious application error.  An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.  Invalid update: invalid number of rows in section 5.  The number of rows contained in an existing section after the update (8) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
2014-04-30 18:52:04.773 uRSS[465:60b] [AppDelegate::saveContextChanges] merged.
2014-04-30 18:52:04.774 uRSS[465:8c03] [AppDelegate::saveContextChanges] merged.
2014-04-30 18:52:04.775 uRSS[465:60b] AFTER: oldcat: trucs (1) / newcat: Misc (8)

我没有得到的是DVC在PerformBlockAndWait循环中工作,但MVC在它们似乎在DVC上提交之前用新值调用。怎么可能?

怎么了?

UPDATE(4): 我按照马库斯的建议做了,并删除了MOC保存。 日志看起来更好(在DVC之前和之后发生,预计在MVC之后发生MVC并且值正确):

2014-05-01 07:27:39.391 uRSS[742:60b] BEFORE: oldcat: Misc (8) / newcat: trucs (1)
2014-05-01 07:27:39.392 uRSS[742:60b] AFTER: oldcat: Misc (7) / newcat: trucs (2)
2014-05-01 07:27:39.400 uRSS[742:60b] MVC: oldcat: Misc (7) / newcat: trucs (2)

但: 我仍然得到错误:

2014-05-01 07:27:39.400 uRSS[742:60b] MasterViewController::didChangeObject **** Moving something from Misc to trucs**** 
2014-05-01 07:27:39.401 uRSS[742:60b] *** Assertion failure in -[UITableView  _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2935.137/UITableView.m:1368
2014-05-01 07:27:39.401 uRSS[742:60b] CoreData: error: Serious application error.  An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.  Invalid update: invalid number of rows in section 5.  The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (8), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)

我还看到,在重新启动应用时,Feed尚未移动,这通常会在保存时发生......

更新(5):我恢复了原来的didChangeObject方法并且可以正常运行(在各类别之间移动Feed,即)。 如何在处理过程中获取“阻止”主UI的保存? performBlockAndWait显然是不够的。

5 个答案:

答案 0 :(得分:2)

NSFetchedResultsController的委托方法告诉UITableView以及UITableViewDataSource的方法告诉表格视图的方式不匹配。

我建议查看您的UITableViewDataSource方法,使用调试器并确保它们报告与NSFetchedResultsControllerDelegate方法相同的行数。

这是此错误的来源。

更新

-controller: didChangeObject: atIndexPath: forChangeType: newIndexPath:UITableViewDataSource中的一种方法之间存在脱节。从错误我猜它是在-tableView: numberOfRowsInSection:,这是我希望你添加到你的问题的方法。

NSFetchedResultsControllerDelegate完成后,它将ping UITableViewDataSource协议实现的方法,并询问表格是什么样的。那些答案必须匹配。如果他们不这样做,则会出现此错误。

如果你在调试器中运行它并在这些方法中放置断点并在一张纸上跟随,你通常可以找到断开连接的位置。

更新

看起来您在Core Data中存储类别打开状态,然后根据类别是否打开来响应行数。希望我正在读那个。

如果是这种情况,那么我建议将该逻辑关闭并查看崩溃是否消失。这将把你的注意力集中在那个逻辑上。您的州很可能不同步。

答案 1 :(得分:1)

虽然不能直接回答您的问题,但您可能会发现此答案可用作构建您自己的可折叠表格视图的替代方法:TLIndexPathTools integration with core data for expandable tableView

正如链接中所讨论的,TLIndexPathTools提供了一个可折叠的表视图实现,可以直接与Core Data集成。

答案 2 :(得分:1)

快速检查 - 您正在实施委托方法:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {

    [self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {

    [self.tableView endUpdates];
}

答案 3 :(得分:1)

在您的NSFetchedResultControllerDelegate方法中,我认为您通过在视图控制器中重复引用属性以及该类别的打开状态来惹恼您。我相信你应该做的就是坚持改变对象。

您的详细信息项目类别与对象不同步。首先将委托方法更改为:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;
    Feed *changedItem = (Feed *)object

    switch(type) {
        case NSFetchedResultsChangeInsert:
            NSLog(@"MasterViewController::didChangeObject **** Inserting something in %@**** ", changedItem.category.name);
            if ([changedItem.category.open boolValue]) {
                [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
            break;

        case NSFetchedResultsChangeDelete:
            NSLog(@"MasterViewController::didChangeObject **** Deleting something in %@**** ", changedItem.category.name);

            if ([_detailViewController.detailCategory.open boolValue]) {
                [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
        break;

        case NSFetchedResultsChangeUpdate:
            NSLog(@"MasterViewController::didChangeObject **** Changing something in %@**** ", changedItem.category.name);

            if ([_detailViewController.detailCategory.open boolValue]) {
                [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            }
            break;

        case NSFetchedResultsChangeMove:
            NSLog(@"MasterViewController::didChangeObject **** Moving something from %@ to %@**** ", _detailViewController.detailCategory.name, _detailViewController.detailItem.category.name);
            // I'm certain that here is where you are out of sync. If changedItem.category
            // is open, then all you need do is insert the moved item. If it is closed
            // you would need to remove all the rows in the category it came from.
            // 
            // The problem arises because category.open state does not necessarily reflect
            // the current state of the section. If the table or section is not yet refreshed
            // then the table state is unknown. We need to know the state of the table at the time
            // of the move in order to know what to do with the rows. On endUpdates the table will
            // call its delegate methods and they must match the results of what we have done here.
            // As a quick fix, given that I do not have all your models or project to work with,
            // We could set all categories open state to closed and collapse all sections prior to making
            // changes here. Better model would be to make the table respond directly any time a
            // category objects open state is changed. You could do that with KVO or NSNotifications perhaps.
            // In that scenario, you could always count on the table and the model to be in sync.

            if ([changedItem.category.open boolValue]) {
                // This becomes way complicated if you have not sync'd open and closed sections to match the category states prior to saving your context.
                [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            } else {
                // Again, if your table sections collapse state is in sync (and only one section
                // is ever open at one time) you need do nothing. Otherwise you'll need some gyrations
                // here to get everything to match what the delegate methods are ultimately going to return
            }

            break;
    }
}

答案 4 :(得分:0)

在我按照以下方式编辑didChangeObject方法后,错误已经消失(我 将开放标记考虑在内):

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
Category *category = ((Feed *) anObject).category;
switch(type) {
    case NSFetchedResultsChangeInsert:
        NSLog(@"MasterViewController::didChangeObject **** Inserting something in %@**** ", category.name);
        if ([category.open boolValue]) {
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
        break;

    case NSFetchedResultsChangeDelete:
        NSLog(@"MasterViewController::didChangeObject **** Deleting something in %@**** ", _detailViewController.detailCategory.name);

        if ([_detailViewController.detailCategory.open boolValue]) {
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
        break;

    case NSFetchedResultsChangeUpdate:
        NSLog(@"MasterViewController::didChangeObject **** Changing something in %@**** ", category.name);

        if ([category.open boolValue]) {
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
        }
        break;

    case NSFetchedResultsChangeMove:
        NSLog(@"MasterViewController::didChangeObject **** Moving something from %@ to %@**** ", _detailViewController.detailCategory.name, category.name);

        if ([_detailViewController.detailCategory.open boolValue]) {
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
        if ([category.open boolValue]) {
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
        break;
}
}