Превышено ли поведение NSFetchedResultsControllerDelegate 'ChangeUpdate'?

Документы для NSFetchedResultsControllerDelegate предоставляют следующий пример кода

- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath {

    UITableView *tableView = self.tableView;

    switch(type) {

        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
            break;

    }

}

Когда я создаю новый объект NSManagedObject, срабатывает NSFetchedResultsChangeInsert (отлично!). Когда я изменяю значение атрибута (используется для заголовка ячейки), срабатывает NSFetchedResultsChangeUpdate. К сожалению, новый заголовок не отображается автоматически, если я не перезаряжу таблицу, раздел или строку. Действительно, если новое имя заставляет результирующий набор сортироваться по-разному, то срабатывает NSFetchedResultsChangeMove, и все хорошо, так как предоставленный код перезагружает весь раздел.

UITableView имеет метод reloadRowsAtIndexPaths: withRowAnimation, поэтому я попытался использовать его в блоке NSFetchedResultsChangeUpdate. Это действительно работает... но документы для этого конкретного метода читаются так, как будто мне это не нужно (обратите внимание на последнюю строку):

Перезагрузка строки вызывает для просмотра своего источника данных для новая ячейка для этой строки. Стол оживляет, что новая ячейка в ней оживляет старую строку. Назовите это метод, если вы хотите предупредить пользователя что значение ячейки меняется. Если, однако, уведомление пользователя не важно, то есть вы просто хотите измените значение, которое ячейка отображение - вы можете получить ячейку для определенную строку и установите ее новое значение.

И да, если я регистрирую, что происходит, когда

[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath]; 

вызывается на NSFetchedResultsChangeUpdate, он может получить последнее значение "name" и установить его в ячейке textLabel. Это имя просто не отображается в ячейке, если я не перезагружу его. Даже если я просто щелкнул по ячейке, появится имя. Обратите внимание, что для воссоздания этого поведения вы должны создать новый управляемый объект, а затем дать ему имя, которое заставляет его сортировать FIRST в NSFetchedResultsController. Таким образом, NSFetchedResultsChangeMove не срабатывает (что работает, так как он перезагружает раздел).

Я что-то упустил или это ожидаемое поведение? "Обсуждение" для reloadRowsAtIndexPaths приводит меня к мысли, что я должен просто установить ячейку textLabel без перезагрузки строки, раздела или таблицы.

Ответ 1

Вы должны вызвать [cell setNeedsLayout] или/и [cell setNeedsDisplay], чтобы вызвать обновление ячейки, в зависимости от реализации вашей ячейки.

Если вы создаете ячейку subviews, как обычно, вы полагаетесь на - layoutSubviews, поэтому вы должны называть [cell setNeedsLayout].

Если вы нарисуете ячейку напрямую с помощью – drawRect:, вы должны вызвать [cell setNeedsDisplay].

Если вы используете как композицию, так и рисунок, вы должны вызвать оба.

Ответ 2

Хотя верно, что вам не нужно перезагружать ячейку, чтобы иметь изменения, вы должны помнить, что iPhone кэширует вид чертежа как можно больше. После того, как вы недавно настроили свою ячейку, вам нужно вызвать setNeedsDisplay в ячейке, чтобы вызвать перерисовку.

Ответ 3

Хотя это явно не указано, вы действительно должны убедиться, что вы включаете методы делегата для контроллераWillChangeContent: и controllerDidChangeContent:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}

и

- (void)controllerDidChangeContent:(BSFetchedResultsController *)controller {
    @try {
        [self.tableView endUpdates];
    }
    @catch (NSException * e) {
        NSLog(@"caught exception: %@: %@", [e name], [e description]);
    }
    @finally { }
}

NSFRC может запускать несколько контроллеров: didChangeObject: atIndexPath: forChangeType: newIndexPath: методы во время любого одного изменения, поэтому не ожидайте, что строка таблицы будет обновляться сразу после каждого из них. Они будут обновляться только после вызова endUpdates в таблице.

Ответ 4

Вызов логики обновления пользовательского интерфейса в MAIN Thread решил мою проблему (оба [cell setNeedsLayout] и [cell setNeedsDisplay] не работают для меня):

...
case NSFetchedResultsChangeUpdate:
    dispatch_async(dispatch_get_main_queue(), {
      [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
    });
    break;
...

Что еще (не об этой проблеме, но будет полезно), лучше выбрать newIndexPath, если она доступна:

...
case NSFetchedResultsChangeUpdate:
    dispatch_async(dispatch_get_main_queue(), {
      NSIndexPath * targetIndexPath = (newIndexPath ?: indexPath)
      [self configureCell:[tableView cellForRowAtIndexPath:targetIndexPath]
              atIndexPath:targetIndexPath];
    });
    break;
...