NSFetchedResultsController:多个FRC,更新时委派错误

目标:使用FRC,通过startDate (一个NSDate属性)对Section's进行sorting,但是希望TodaySection出现在Upcoming dates Section之前。

我使用瞬态属性sectionIdentifier跟踪了Apple的代码。 苹果的示例代码 。 并首先从这个项目开始: OneFRC

我很快意识到,这可能不可能与一个FRC(我可能是错的)。

接下来,我决定用3 FRCs 刺杀这个ThreeFRC

TableView sections现在显示在我想要的订单中:

第0部分: Today

第一部分: Upcoming

第二部分: Past

但是,添加数据会触发FRC委托,并且出现以下错误:

 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 0. The number of rows contained in an existing section after the update (4) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 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) 

再次,我希望能够用1 FRC来实现我的目标 ,但我似乎无法弄清楚如何。

我一直试图解决这个问题已经有4天了! 如果这个问题没有得到解决,我想我可以联系苹果公司获得开发者支持。 如果我这样做,我会在这里发布决议,以便其他人可以受益。

项目在Github上可用:

一个FRC

三FRC

编辑

感谢@blazejmar,我能够摆脱rows错误。 但是,现在我试图添加sections时出现错误。

 2014-11-03 16:39:46.852 FRC[64305: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 sections. The number of sections contained in the table view after the update (2) must be equal to the number of sections contained in the table view before the update (1), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted). with userInfo (null) 

在三个FRC中重现错误的步骤:

 1. Launch App -> 2. Tap Generate Data -> 3. Tap View in FRC -> 4. Tap back to the RootVC -> 5. Change the system date to a month from Today -> 6. Tap View in FRC and only one section `Past` should appear. -> 7. Tap `Add Data`. 8. The error should appear in the log. 

在你的ThreeFRC项目中有一些问题:

 - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { [self.tableView beginUpdates]; self.numberOfSectionsInTV = 0; [self fetchData]; } - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { [self.tableView reloadData]; [self.tableView endUpdates]; } 

你不应该在FRC委托中使用fetchData。 方法以正确的顺序调用(在更新之前,期间和之后),所以在callback函数中你有一致的上下文状态。 另外,在endUpdates之前使用reloadData并不是最好的办法(它应用了前面提供的所有更改),而reloadData则是擦除所有内容并从头开始构build它。 这很可能导致崩溃。

我发现的其他事情可能是越野车处理更新。 如果您有3个单独的FRC没有部分,您将不会在FRC委托中获取部分更新callback。 但是,如果某些对象出现在其中一个FRC中,则应该检测到并手动插入它们。

在controllerDidChangeContent中只使用reloadData就足够了,但这不是最好的解决scheme,因为你不会得到任何animation。 正确的方法是处理所有的情况:从FRC删除所有对象(然后从TableView手动删除部分),将第一个对象插入FRC(然后在适当的indexPath处创build新的部分)。

我看着你的ThreeFRC项目,注意到- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView你检查哪些FRCs包含对象,这将确定部分的数量。 这是合乎逻辑的,但是在添加/删除“部分”(或者当其他FRC突然有对象时)时,FRC委托确实会令人困惑。 例如,您只有“过去”部分(1部分),但是数据发生变化,您现在也有“今天”部分。 由于sectionPastFRC或其他FRC没有任何节更改,所以没有调用- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type ,虽然现在应该有2个部分,但没有呼叫要添加,删除或移动部分。 你不得不手动更新这些部分,这可能是一个痛苦。

这里是我build议的解决方法:因为你将永远最多只有一个部分为每个FRC,你应该只返回3在- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 。 因此,添加/删除部分将不再有任何问题,因为他们都已经在那里了。 无论如何,如果,例如,今天部分没有对象,只需返回0在- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 。 只要确保在- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section ,如果fetchedObjects == 0,则返回nil,这样它也不会显示节标题,如果该节没有对象。 并且在FRC委托didChangeObject中,只需在对tableView执行更改之前始终调整indexPath和newIndexPath。

请注意,如果您已经知道FRC(除最后一个FRC之外)将需要的最大部分数量,则此解决方法才有效。 它不是一个表格视图中多个FRC的所有实现的解决scheme。 实际上,我在一个项目中使用了这个解决scheme,其中一个tableView有2个FRC,但是第一个FRC只占用1个部分,而第二个FRC可以有任意数量的部分。 我总是不得不调整第二个FRC的变化部分+1。

我实际上已经尝试应用我上面提到的更改到您的代码,并没有得到错误。 这里是我在UITableViewDataSource中更改的部分:

 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 3; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSInteger rows = 0; switch (section) { case 0: { rows = [[self.sectionTodayFRC fetchedObjects]count]; break; } case 1: { rows = [[self.sectionUpcomingFRC fetchedObjects]count]; break; } case 2: { rows = [[self.sectionPastFRC fetchedObjects]count]; break; } } NSLog(@"Section Number: %i Number Of Rows: %i", section,rows); return rows; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *header; switch (section) { case 0: { if ([[self.sectionTodayFRC fetchedObjects]count] >0) { header = @"Today"; } break; } case 1: { if ([[self.sectionUpcomingFRC fetchedObjects]count] >0) { header = @"Upcoming"; } break; } case 2: { if ([[self.sectionPastFRC fetchedObjects]count] >0) { header = @"Past"; } break; } } return header; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } Meeting *meeting; switch (indexPath.section) { case 0: if ([[self.sectionTodayFRC fetchedObjects]count] > 0) { meeting = [[self.sectionTodayFRC fetchedObjects] objectAtIndex:indexPath.row]; } break; case 1: if ([[self.sectionUpcomingFRC fetchedObjects]count] > 0) { meeting = [[self.sectionUpcomingFRC fetchedObjects] objectAtIndex:indexPath.row]; } break; case 2: if ([[self.sectionPastFRC fetchedObjects]count] > 0) { meeting = [[self.sectionPastFRC fetchedObjects] objectAtIndex:indexPath.row]; } break; } cell.textLabel.text = meeting.title; return cell; } 

和NSFetchedResultsControllerDelegate:

 - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { NSLog(@"Inside didChangeObject:"); NSIndexPath *modifiedIndexPath; NSIndexPath *modifiedNewIndexPath; if (controller == self.sectionTodayFRC) { modifiedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:0]; modifiedNewIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row inSection:0]; } else if (controller == self.sectionUpcomingFRC) { modifiedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:1]; modifiedNewIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row inSection:1]; } else if (controller == self.sectionPastFRC) { modifiedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:2]; modifiedNewIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row inSection:2]; } switch(type) { case NSFetchedResultsChangeInsert: [self.tableView insertRowsAtIndexPaths:@[modifiedNewIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: NSLog(@"frcChangeDelete"); [self.tableView deleteRowsAtIndexPaths:@[modifiedIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeUpdate: NSLog(@"frcChangeUpdate"); [self.tableView reloadRowsAtIndexPaths:@[modifiedIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeMove: NSLog(@"frcChangeDelete"); [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:modifiedIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:modifiedNewIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; } } 

我希望这可以帮助别人!