Проблема с обновлениями NSFetchedResultsController и отношением «один к одному» - PullRequest
4 голосов
/ 12 июля 2011

У меня возникли проблемы со вставками, использующими NSFetchedResultsController с простым отношением «один к одному».Когда я создаю новый объект Source, который имеет отношение «один к одному» с объектом Target, он, кажется, вызывает - контроллер ((void): (NSFetchedResultsController *) controller didChangeObject ...] дважды с обоими типами NSFetchedResultsChangeInsert и NSFetchedResultsChangeUpdate,что приводит к тому, что представление таблицы отображает неточные данные сразу после обновления.

Я могу воссоздать это на простом примере, основанном на стандартном шаблоне проекта, который XCode генерирует в приложении CoreData на основе навигации.Шаблон создает объект Event с атрибутом timeStamp.Я хочу добавить новую сущность «Tag» к этому событию, которая представляет собой отношение «1: 1» с Entity, идея в том, что каждое событие имеет определенный тег из некоторого списка тегов.Я создаю связь между событием и тегом в редакторе основных данных и обратную связь между тегом и событием.Затем я генерирую подклассы 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;

Затем я предварительно заполнял сущность Tags некоторыми данными при запуске, чтобы мы могли выбирать из тегапри вставке нового события.В 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, чтобы добавить тег в атрибут 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();
    }
}

Теперь я хочу увидеть табличное представление, отражающее эти изменения, поэтому я сделал 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, создается впечатление, что первая строка в порядке, но вторая строка является дубликатом первой, хотя временная метка должна быть на расстоянии нескольких секунд: enter image description here

Когда я прокручиваю экранвверх и вниз он обновляет строки, а затем отображает правильные результаты с правильным временем.Когда я перебираю код, возникает основная проблема: вставка новой строки, кажется, вызывает вызывающий [(NSFetchedResultsController *) controller didChangeObject ...] дважды, один раз для вставки и один раз для обновления.Я не уверен, ПОЧЕМУ обновление называется хотя.И вот решающий аргумент: если я уберу обратную связь между Event и Tag, вставки начнут работать просто отлично!Вызывается только вставка, строка не дублируется, и все работает хорошо.

Так что же такое с обратной зависимостью, которая вызывает двойной вызов методов делегата NSFetchedResultsController?И я должен просто жить без них в этом случае?Я знаю, что XCode выдает предупреждение, если не указано обратное, и это кажется плохой идеей.Я что-то здесь не так делаю?Это известная проблема с известным обходным решением?

Спасибо.

Ответы [ 4 ]

1 голос
/ 06 января 2013

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

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

1 голос
/ 27 мая 2013

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

case NSFetchedResultsChangeUpdate:
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    break;
0 голосов
/ 11 мая 2017

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

0 голосов
/ 10 марта 2012

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

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

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

Задержка сохранения контекста в последующем цикле выполнения, похоже, является еще одним решением дляпример:

dispatch_async(dispatch_get_main_queue(), ^{ [context save:nil]; });
...