自定义部分名称导致 NSFetchedResultsController 崩溃
我有一个带有 dueDate 属性的托管对象。我没有使用一些丑陋的日期字符串作为 UITableView 的节标题进行显示,而是创建了一个名为“category”的瞬态属性,并像这样定义它:
- (NSString*)category
{
[self willAccessValueForKey:@"category"];
NSString* categoryName;
if ([self isOverdue])
{
categoryName = @"Overdue";
}
else if ([self.finishedDate != nil])
{
categoryName = @"Done";
}
else
{
categoryName = @"In Progress";
}
[self didAccessValueForKey:@"category"];
return categoryName;
}
这是 NSFetchedResultsController 设置:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Task"
inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSMutableArray* descriptors = [[NSMutableArray alloc] init];
NSSortDescriptor *dueDateDescriptor = [[NSSortDescriptor alloc] initWithKey:@"dueDate"
ascending:YES];
[descriptors addObject:dueDateDescriptor];
[dueDateDescriptor release];
[fetchRequest setSortDescriptors:descriptors];
fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:@"category" cacheName:@"Root"];
该表最初显示正常,显示其 dueDate 已完成的未完成项目标题为“进行中”的部分未通过。现在,用户可以点击表视图中的一行,将新的详细信息视图推送到导航堆栈上。在这个新视图中,用户可以点击按钮来指示该项目现在已“完成”。这是按钮的处理程序(self.task 是托管对象):
- (void)taskDoneButtonTapped
{
self.task.finishedDate = [NSDate date];
}
一旦“finishedDate”属性的值发生更改,我就会遇到此异常:
2010-03-18 23:29:52.476 MyApp[1637:207] Serious application error. Exception was caught during Core Data change processing: no section named 'Done' found with userInfo (null)
2010-03-18 23:29:52.477 MyApp[1637:207] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no section named 'Done' found'
我设法找出当前的 UITableView被新的详细信息视图隐藏的正在尝试更新其行和部分,因为 NSFetchedResultsController 被通知数据集中发生了某些变化。这是我的表更新代码(从 Core Data Recipes 示例或 CoreBooks 示例复制 - 我不记得是哪个):
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
我在每个函数中放置了断点,发现只调用了controllerWillChange。在调用controller:didChangeObject:atIndexPath:forChangeType:newIndex 或controller:didChangeSection:atIndex:forChangeType 之前引发异常。
此时我被困住了。如果我将我的sectionNameKeyPath更改为“dueDate”,那么一切正常。我认为这是因为 dueDate 属性永远不会改变,而在 finishDate 属性更改后读回时类别会有所不同。
请帮忙!
更新:
这是我的 UITableViewDataSource 代码:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}
I have a managed object with a dueDate attribute. Instead of displaying using some ugly date string as the section headers of my UITableView I created a transient attribute called "category" and defined it like so:
- (NSString*)category
{
[self willAccessValueForKey:@"category"];
NSString* categoryName;
if ([self isOverdue])
{
categoryName = @"Overdue";
}
else if ([self.finishedDate != nil])
{
categoryName = @"Done";
}
else
{
categoryName = @"In Progress";
}
[self didAccessValueForKey:@"category"];
return categoryName;
}
Here is the NSFetchedResultsController set up:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Task"
inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSMutableArray* descriptors = [[NSMutableArray alloc] init];
NSSortDescriptor *dueDateDescriptor = [[NSSortDescriptor alloc] initWithKey:@"dueDate"
ascending:YES];
[descriptors addObject:dueDateDescriptor];
[dueDateDescriptor release];
[fetchRequest setSortDescriptors:descriptors];
fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:@"category" cacheName:@"Root"];
The table initially displays fine, showing the unfinished items whose dueDate has not passed in a section titled "In Progress". Now, the user can tap a row in the table view which pushes a new details view onto the navigation stack. In this new view the user can tap a button to indicate that the item is now "Done". Here is the handler for the button (self.task is the managed object):
- (void)taskDoneButtonTapped
{
self.task.finishedDate = [NSDate date];
}
As soon as the value of the "finishedDate" attribute changes I'm hit with this exception:
2010-03-18 23:29:52.476 MyApp[1637:207] Serious application error. Exception was caught during Core Data change processing: no section named 'Done' found with userInfo (null)
2010-03-18 23:29:52.477 MyApp[1637:207] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no section named 'Done' found'
I've managed to figure out that the UITableView that is currently hidden by the new details view is trying to update its rows and sections because the NSFetchedResultsController was notified that something changed in the data set. Here's my table update code (copied from either the Core Data Recipes sample or the CoreBooks sample -- I can't remember which):
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
I put breakpoints in each of these functions and found that only controllerWillChange is called. The exception is thrown before either controller:didChangeObject:atIndexPath:forChangeType:newIndex or controller:didChangeSection:atIndex:forChangeType are called.
At this point I'm stuck. If I change my sectionNameKeyPath to just "dueDate" then everything works fine. I think that's because the dueDate attribute never changes whereas the category will be different when read back after the finishedDate attribute changes.
Please help!
UPDATE:
Here is my UITableViewDataSource code:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
崩溃是由于 NSFetchedResultsController 事先不知道“完成”类别而导致崩溃。我在其他问题中多次看到过这种崩溃,对于每个问题,我都建议向 Apple 提交雷达票。这是 NSFetchedResultsController 中的一个错误。
The crash is being caused by the NSFetchedResultsController not knowing about the "done" category before hand and therefore crashing. I have seen this crash a few other times in other questions and with each one I recommend submitting a radar ticket to Apple. This is a bug in the
NSFetchedResultsController
.在我看来,您的问题在于您用来提供
sectionNameKeyPath
的“类别”瞬态属性。sectionNameKeyPath
的顺序必须与主排序描述符相同。在您的情况下,这意味着所有“逾期”任务的日期必须早于所有“完成”任务的日期必须早于所有“进行中”任务的日期。可以构建一个场景,其中“完成”任务的dueDate
位于“进行中”任务之后或“逾期”任务之前。这种情况破坏了sectionNameKeyPath
的排序要求,并导致NSFetchedResultsController
抛出NSInternalConsistencyException
。我针对您的问题提出了一个解决方案,该解决方案不涉及滚动您自己的数组,然后必须将其分成几个部分。在模型中创建一个整数属性,其中将 0 映射到“逾期”,1 映射到“完成”,2 映射到“进行中”。将此属性设为
NSFetchRequest
中的主要排序描述符,并按升序对此属性进行排序。向NSFetchRequest
添加辅助排序描述符,以升序对dueDate
属性进行排序。修改您的类别方法,从上面创建的整数属性派生类别名称,并将其用作您的sectionNameKeyPath
。您将需要更新整数属性来更新任务,因为它们从正在进行到逾期完成等。It looks to me like your problem lies with your "category" transient property that you are using to supply the
sectionNameKeyPath
. ThesectionNameKeyPath
must order the same as the primary sort descriptor. In your case, this means that all "Overdue" tasks MUST have dates earlier than all "Done" tasks MUST have dates earlier than all "In Progress" tasks. It is possible to construct a scenario where a "Done" task has adueDate
that comes after an "In Progress" task or comes before an "Overdue" task. This scenario breaks the ordering requirement of thesectionNameKeyPath
and causes theNSFetchedResultsController
to throw anNSInternalConsistencyException
.I propose a solution to your problem that doesn't involve rolling your own array which then must be split into sections. Create an integer attribute in your model where you map 0 to "Overdue", 1 to "Done", and 2 to "In Progress". Make this the primary sort descriptor in your
NSFetchRequest
and sort this property in ascending order. Add a secondary sort descriptor to theNSFetchRequest
that sorts thedueDate
property in ascending order. Modify your category method to derive category names from the integer attribute you created above and use that as yoursectionNameKeyPath
. You will need to update the integer attribute to update tasks as they move from in progress to overdue to done, etc.解决这个问题的一种巧妙方法是检查它是否是部分中的第一行,然后不重新加载该行
,但更安全的方法是删除 switch case 并将其替换为:
A hacky way around this is to check if it's the first row in section and then don't reload the row
but a safer approach would be to remove the switch case and replace it with: