Проблема с обновлениями NSFetchedResultsController и отношением к одному

У меня возникли проблемы со вставками с использованием 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 выдает предупреждение, если инверсия не указана, и это кажется плохой идеей. Я делаю что-то не так здесь? Это какая-то известная проблема с известным обходным путем?

Спасибо.


person Z S    schedule 12.07.2011    source источник
comment
Любое обновление для этого? Я нахожу похожую проблему. Я получаю 1 уведомление об изменении контекста, и мой извлеченный делегат контроллера результатов получает дважды.   -  person Brian King    schedule 19.09.2011


Ответы (4)


Что касается многократного вызова didChangeObject, я нашел одну причину, по которой это произойдет. Если в вашем контроллере есть несколько NSFetchedResultsController, которые совместно используют NSManagedObjectContext, didChangeObject будет вызываться несколько раз, когда что-то изменится с данными. Я наткнулся на эту же проблему, и после серии тестов я заметил именно такое поведение. Однако я не проверял, произойдет ли такое поведение, если NSFetchedResultsControllers не разделяет NSManagedObjectContext. К сожалению, didChangeObject не сообщает, какой NSFetchedResultsController запустил обновление. Чтобы достичь своей цели, я использовал флаг в своем коде.

Надеюсь это поможет!

person apisip    schedule 06.01.2013

Вы можете использовать [tableView reloadRowsAtIndexPaths:withRowAnimation:] для NSFetchedResultsChangeUpdate вместо метода configureCell.

case NSFetchedResultsChangeUpdate:
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    break;
person Darcy    schedule 27.05.2013

У меня такая же проблема. И есть решение. При определенных обстоятельствах NSFetchedResultsController запускается дважды при вызове -(BOOL)save: в контексте управляемого объекта, непосредственно после вставки или манипулирования.

В моем случае я творю магию с объектом в методе NSManagedObject -(void)willSave, в результате чего NSFetchedResultsController срабатывает дважды. Кажется, это ошибка.

Чтобы не манипулировать вставленными объектами во время сохранения, мне помогло!

Другим решением, по-видимому, является отсрочка сохранения контекста в более позднем цикле выполнения, например:

dispatch_async(dispatch_get_main_queue(), ^{ [context save:nil]; });
person Ingo Kasprzak    schedule 10.03.2012

Объекты в NSFetchedResultsController должны быть вставлены с постоянным идентификатором объекта. После создания объекта и перед сохранением в постоянном хранилище он имеет временный идентификатор объекта. После сохранения объекта получите постоянный objectID. Если объект с временным objectID вставлен в NSFetchedResultsController, то после сохранения объекта и изменения его objectID на постоянный контроллер NSFetchedResults может сообщить о вставке поддельного дубликата объекта. Решение после создания экземпляра объекта, который будет извлечен в NSFetchedResultsController, — просто вызовите с ним obtainPermanentIDsForObjects в его manageObjectContext.

person poGUIst    schedule 11.05.2017