我在使用NSFetchedResultsController与简单的一对一关系的插入时遇到了一些麻烦。当我创建一个新的Source对象,它与Target对象有一对一的关系时,似乎两次调用 - [(void)controller:(NSFetchedResultsController *)controller didChangeObject ...],同时使用NSFetchedResultsChangeInsert和NSFetchedResultsChangeUpdate类型,这导致tableview在更新后立即显示不准确的数据。
我可以使用一个基于XCode在基于导航的CoreData应用程序中生成的标准模板项目的简单示例来重新创建它。该模板创建一个具有timeStamp属性的Event实体。我想为这个事件添加一个新的实体“Tag”,它与Entity只是一对一的关系,这个想法是每个事件都有一些标签列表中的特定标签。我在Core Data编辑器中创建了从Event到Tag的关系,以及从Tag到Event的反向关系。然后我为Event和Tag生成NSManagedObject子类,这是非常标准的:
@interface Event : NSManagedObject {
@private
}
@property (nonatomic, retain) NSDate * timeStamp;
@property (nonatomic, retain) Tag * tag;
and
@interface Tag : NSManagedObject {
@private
}
@property (nonatomic, retain) NSString * tagName;
@property (nonatomic, retain) NSManagedObject * event;
然后我在发布时用一些数据预填充了Tags实体,这样我们就可以在插入新事件时从Tag中选择。在AppDelegate中,在返回persistentStoreCoordinator:
之前调用它NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Tag" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
//check if Tags haven't already been created. If not, then create them
if (fetchedObjects.count == 0) {
NSLog(@"create new objects for Tag");
Tag *newManagedObject1 = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:context];
newManagedObject1.tagName = @"Home";
Tag *newManagedObject2 = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:context];
newManagedObject2.tagName = @"Office";
Tag *newManagedObject3 = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:context];
newManagedObject3.tagName = @"Shop";
}
[fetchRequest release];
if (![context save:&error])
{
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
现在,我更改了insertNewObject代码,将Tag添加到我们正在插入的Event属性中。我只是从这个例子的fetchedObjects列表中选择第一个:
- (void)insertNewObject
{
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
Event *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
// If appropriate, configure the new managed object.
// Normally you should use accessor methods, but using KVC here avoids the need to add a custom class to the template.
[newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entityTag = [NSEntityDescription entityForName:@"Tag" inManagedObjectContext:context];
[fetchRequest setEntity:entityTag];
NSError *errorTag = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&errorTag];
if (fetchedObjects.count > 0) {
Tag *newtag = [fetchedObjects objectAtIndex:0];
newManagedObject.tag = newtag;
}
// Save the context.
NSError *error = nil;
if (![context save:&error])
{
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
我想现在看到反映这些更改的tableview,所以我让UITableViewCell键入UITableViewCellStyleSubtitle并更改了configureCell以在详细文本标签中显示tagName:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
Event *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [[managedObject valueForKey:@"timeStamp"] description];
cell.detailTextLabel.text = managedObject.tag.tagName;
}
现在一切都已到位。当我调用insertNewObject时,它似乎创建第一行正常,但第二行是第一行的副本,即使时间戳应该相隔几秒:
当我上下滚动屏幕时,它会刷新行,然后以正确的时间显示正确的结果。当我单步执行代码时,核心问题出现了:插入一个新行似乎是两次调用[(NSFetchedResultsController *)控制器didChangeObject ...],一次用于插入,一次用于更新。我不确定为什么要调用更新。这是关键:如果我删除事件和标记之间的反向关系,插入开始工作就好了!只调用插入,行不重复,并且运行良好。
那么反向关系是什么导致NSFetchedResultsController委托方法被调用两次呢?在这种情况下我应该没有他们吗?我知道如果没有指定反转,XCode会发出警告,这似乎是一个坏主意。我在这里做错了吗?这是一个已知的解决方案吗?
感谢。
答案 0 :(得分:1)
关于多次调用didChangeObject
,我发现了这种情况将会发生的原因之一。如果控制器中有多个NSFetchedResultsController
共享NSManagedObjectContext
,则当数据发生变化时,didChangeObject
将被多次调用。我偶然发现了同样的问题,经过一系列的测试后,这就是我注意到的行为。我没有测试过,如果NSFetchedResultsControllers
不共享NSManagedObjectContext
,这种行为是否会发生。不幸的是,didChangeObject
并未告知哪个NSFetchedResultsController
触发了更新。为了实现我的目标,我最终在我的代码中使用了一个标志。
希望这有帮助!
答案 1 :(得分:1)
您可以将[tableView reloadRowsAtIndexPaths:withRowAnimation:]用于 NSFetchedResultsChangeUpdate ,而不是 configureCell 方法。
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
答案 2 :(得分:0)
我遇到了同样的问题。并有一个解决方案。
在某些情况下,NSFetchedResultsController
在插入或操作后直接调用托管对象上下文中的-(BOOL)save:
时会被触发两次。
就我而言,我正在使用NSManagedObject - (void)willSave方法中的对象做一些魔术,这会导致NSFetchedResultsController
触发两次。这似乎是一个错误。
在保存时不操纵插入的物体对我来说很有用!
将上下文保存延迟到以后的运行循环似乎是另一种解决方案,例如:
dispatch_async(dispatch_get_main_queue(), ^{ [context save:nil]; });
答案 3 :(得分:0)
NSFetchedResultsController中的对象必须插入永久objectID。在创建对象之后,在保存到持久性存储之前,它具有临时objectID。保存对象后接收永久对象ID。如果将带有临时objectID的对象插入到NSFetchedResultsController中,则在保存对象并将其objectID更改为permanent后,NSFetchedResults控制器可能会报告有关插入伪重复对象的情况。 在实例化将在NSFetchedResultsController中获取的对象后的解决方案 - 只需用它调用 obtainPermanentIDsForObjects on managedObjectContext 。