У меня возникли проблемы со вставками с использованием NSFetchedResultsController с простым отношением к одному. Когда я создаю новый исходный объект, который имеет однозначную связь с целевым объектом, он, кажется, дважды вызывает - [(void)controller:(NSFetchedResultsController *)controller didChangeObject...] с типами NSFetchedResultsChangeInsert и NSFetchedResultsChangeUpdate, из-за чего табличное представление отображает неточные данные сразу после обновления.
Я могу воссоздать это с помощью простого примера на основе стандартного проекта шаблона, который XCode генерирует в приложении CoreData на основе навигации. Шаблон создает сущность Event с атрибутом timeStamp. Я хочу добавить к этому событию новый объект «Тег», который представляет собой просто отношение 1 к 1 с объектом, идея заключается в том, что каждое событие имеет определенный тег из некоторого списка тегов. Я создаю отношение от события к тегу в редакторе Core Data и обратное отношение от тега к событию. Затем я генерирую подклассы NSManagedObject как для Event, так и для Tag, которые довольно стандартны:
@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;
Затем я предварительно заполнил объект «Теги» некоторыми данными при запуске, чтобы мы могли выбирать тег при вставке нового события. В AppDelegate вызовите это перед возвращением persistenceStoreCoordinator:
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, чтобы добавить тег к атрибуту события, который мы вставляем. Я просто выбираю первый из списка 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();
}
}
Теперь я хочу, чтобы табличное представление отражало эти изменения, поэтому я сделал 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 *)controller didChangeObject ...] дважды, один раз для вставки и один раз для обновления. Я не уверен, ПОЧЕМУ вызывается обновление. И вот ключевой момент: если я удалю обратную связь между Событием и Тегом, вставки начнут работать нормально! Вызывается только вставка, строка не дублируется, и все работает хорошо.
Так что же такого в обратном отношении, из-за которого методы делегата NSFetchedResultsController вызываются дважды? И должен ли я просто жить без них в этом случае? Я знаю, что XCode выдает предупреждение, если инверсия не указана, и это кажется плохой идеей. Я делаю что-то не так здесь? Это какая-то известная проблема с известным обходным путем?
Спасибо.