Изменение структуры UITableView с использованием основных данных

Возможный дубликат:
Как реализовать изменение порядка записей CoreData?

Я пытаюсь найти образец кода, который показывает, как обрабатывать перемещение / перестановку ячеек в tableView, когда ячейка использует fetchedResultsController (т.е. в сочетании с Core Data). Я получаю вызов moveRowAtIndexPath: к моему источнику данных, но не могу найти правильную комбинацию черной магии, чтобы таблица / данные правильно распознали изменение.

Например, когда я перемещаю строку 0 в строку 2, а затем отпускаю ее, она "выглядит" правильно. Затем нажимаю «Готово». Строка (1), которая сдвинулась вверх, чтобы заполнить строку 0, по-прежнему имеет вид в режиме редактирования (значки минус и перемещение), в то время как другие строки ниже возвращаются к обычному виду. Если я затем прокручиваю вниз, поскольку строка 2 (изначально 0, помните?) Приближается к верху, она полностью исчезает.

Какого черта. Нужно ли мне каким-то образом аннулировать fetchedResultsController? Всякий раз, когда я устанавливаю его на ноль, я получаю вылеты. Должен ли я выпустить его вместо этого? Я в сорняках?

Вот что у меня там сейчас ...

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {

    NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];

    /*
     Update the links data in response to the move.
     Update the display order indexes within the range of the move.
     */

    if (fromIndexPath.section == toIndexPath.section) {

        NSInteger start = fromIndexPath.row;
        NSInteger end = toIndexPath.row;
        NSInteger i = 0;
        if (toIndexPath.row < start)
            start = toIndexPath.row;
        if (fromIndexPath.row > end)
            end = fromIndexPath.row;
        for (i = start; i <= end; i++) {
            NSIndexPath *tempPath = [NSIndexPath indexPathForRow:i inSection:toIndexPath.section];
            LinkObj *link = [fetchedResultsController objectAtIndexPath:tempPath];
            //[managedObjectContext deleteObject:[fetchedResultsController objectAtIndexPath:tempPath]];
            link.order = [NSNumber numberWithInteger:i];
            [managedObjectContext refreshObject:link mergeChanges:YES];
            //[managedObjectContext insertObject:link];
        }

    }
    // Save the context.
    NSError *error;
    if (![context save:&error]) {
        // Handle the error...
    }

}

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {

    // The fetch controller is about to start sending change notifications, so prepare the table view for updates.
    if (self.theTableView != nil)
        [self.theTableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller has sent all current change notifications, so tell the table view to process all updates.
    if (self.theTableView != nil) {
        [self.theTableView endUpdates];
    }
}

person Greg Combs    schedule 20.07.2009    source источник
comment
Я решил это немного по-другому (опубликую, когда он будет протестирован). Для этого действительно нужен Reverse-NSFetchedResultsController. По сути, подайте NSFetchedResultsController UITableView, порядок сортировки и заголовки разделов, и пусть он все записывает обратно в CoreData. Думаю, я мог бы попытаться собрать это для развлечения.   -  person Corey Floyd    schedule 15.09.2009
comment
Абсолютно ... что-то более элегантное, чем мое решение, которое обеспечит двунаправленную синхронизацию между сохраненными данными и пользовательским интерфейсом таблицы. Я хотел бы увидеть, что вы придумали, когда закончите.   -  person Greg Combs    schedule 21.09.2009
comment
Как бы то ни было, Мэтт Лонг недавно опубликовал хороший учебник по переупорядочению строк в таблице, поддерживаемой NSFetchedResultsController: cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller   -  person Clint Harris    schedule 22.07.2010
comment
См. Это простое решение: stackoverflow.com/a/15625897/308315   -  person iwasrobbed    schedule 26.03.2013


Ответы (8)


Обычно, когда вы видите подобные артефакты, пользовательский интерфейс был анимирован в новое положение и сообщил вам об этом, тогда обновления, которые вы сделали для своей модели, неправильно отражают состояние, которое приводит к сбоям в следующий раз, когда view должен ссылаться на модель для обновления.

Я думаю, вы не совсем понимаете, что вы должны делать в этом методе. Он вызывается, потому что пользовательский интерфейс изменился, и он должен позволить модели измениться соответствующим образом. В приведенном ниже коде предполагается, что результаты уже находятся в новом порядке, и вам просто нужно по какой-то причине сбросить поле заказа:

    for (i = start; i <= end; i++) {
            NSIndexPath *tempPath = [NSIndexPath indexPathForRow:i inSection:toIndexPath.section];
            LinkObj *link = [fetchedResultsController objectAtIndexPath:tempPath];
            //[managedObjectContext deleteObject:[fetchedResultsController objectAtIndexPath:tempPath]];
            link.order = [NSNumber numberWithInteger:i];
            [managedObjectContext refreshObject:link mergeChanges:YES];
            //[managedObjectContext insertObject:link];
    }

Загвоздка в том, что вы фактически не меняете порядок в базовой модели. Эти indexPaths взяты из UITableViewController, он сообщает вам, что пользователь перетаскивал их между ними, и вам необходимо обновить соответствующие данные. Но fetchedResultsController всегда находится в порядке сортировки, поэтому, пока вы не измените эти свойства, ничего не перемещается.

Дело в том, что они не были перемещены, вас вызывают, чтобы сказать вам, что вам нужно переместить их (путем настройки свойства sortable). Вам действительно нужно что-то вроде:

NSNumber *targetOrder = [fetchedResultsController objectAtIndexPath:toIndexPath];
LinkObj *link = [fetchedResultsController objectAtIndexPath:FromPath];
link.order = targetOrder;

Это заставит объекты переупорядочить, а затем пройти и очистить любой из порядковых номеров других объектов, которые должны были сдвинуться вверх, зная, что индексы могли переместиться.

person Louis Gerbarg    schedule 20.07.2009

Вот то, что официально работает сейчас, с удалениями, перемещениями и вставками. Я «подтверждаю» заказ каждый раз, когда на него влияет действие редактирования.

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section != kHeaderSection) {

        if (editingStyle == UITableViewCellEditingStyleDelete) {

            @try {
                LinkObj * link = [self.fetchedResultsController objectAtIndexPath:indexPath];

                debug_NSLog(@"Deleting at indexPath %@", [indexPath description]);
            //debug_NSLog(@"Deleting object %@", [link description]);

                if ([self numberOfBodyLinks] > 1) 
                    [self.managedObjectContext deleteObject:link];

            }
            @catch (NSException * e) {
                debug_NSLog(@"Failure in commitEditingStyle, name=%@ reason=%@", e.name, e.reason);
            }

        }
        else if (editingStyle == UITableViewCellEditingStyleInsert) {
            // we need this for when they click the "+" icon; just select the row
            [theTableView.delegate tableView:tableView didSelectRowAtIndexPath:indexPath];
        }
    }
}

- (BOOL)validateLinkOrders {        
    NSUInteger index = 0;
    @try {      
        NSArray * fetchedObjects = [self.fetchedResultsController fetchedObjects];

        if (fetchedObjects == nil)
            return NO;

        LinkObj * link = nil;       
        for (link in fetchedObjects) {
            if (link.section.intValue == kBodySection) {
                if (link.order.intValue != index) {
                    debug_NSLog(@"Info: Order out of sync, order=%@ expected=%d", link.order, index);

                    link.order = [NSNumber numberWithInt:index];
                }
                index++;
            }
        }
    }
    @catch (NSException * e) {
        debug_NSLog(@"Failure in validateLinkOrders, name=%@ reason=%@", e.name, e.reason);
    }
    return (index > 0 ? YES : NO);
}


- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
    NSArray * fetchedObjects = [self.fetchedResultsController fetchedObjects];  
    if (fetchedObjects == nil)
        return;

    NSUInteger fromRow = fromIndexPath.row + NUM_HEADER_SECTION_ROWS;
    NSUInteger toRow = toIndexPath.row + NUM_HEADER_SECTION_ROWS;

    NSInteger start = fromRow;
    NSInteger end = toRow;
    NSInteger i = 0;
    LinkObj *link = nil;

    if (toRow < start)
        start = toRow;
    if (fromRow > end)
        end = fromRow;

    @try {

        for (i = start; i <= end; i++) {
            link = [fetchedObjects objectAtIndex:i]; //
            //debug_NSLog(@"Before: %@", link);

            if (i == fromRow)   // it's our initial cell, just set it to our final destination
                link.order = [NSNumber numberWithInt:(toRow-NUM_HEADER_SECTION_ROWS)];
            else if (fromRow < toRow)
                link.order = [NSNumber numberWithInt:(i-1-NUM_HEADER_SECTION_ROWS)];        // it moved forward, shift back
            else // if (fromIndexPath.row > toIndexPath.row)
                link.order = [NSNumber numberWithInt:(i+1-NUM_HEADER_SECTION_ROWS)];        // it moved backward, shift forward
            //debug_NSLog(@"After: %@", link);
        }
    }
    @catch (NSException * e) {
        debug_NSLog(@"Failure in moveRowAtIndexPath, name=%@ reason=%@", e.name, e.reason);
    }
}


- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {    
    @try {
        switch (type) {
            case NSFetchedResultsChangeInsert:
                [theTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
                [self validateLinkOrders];
                break;
            case NSFetchedResultsChangeUpdate:
                break;
            case NSFetchedResultsChangeMove:
                self.moving = YES;
                [self validateLinkOrders];
                break;
            case NSFetchedResultsChangeDelete:
                [theTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                [self validateLinkOrders];
                break;
            default:
                break;
        }
    }
    @catch (NSException * e) {
        debug_NSLog(@"Failure in didChangeObject, name=%@ reason=%@", e.name, e.reason);
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.theTableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.theTableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller has sent all current change notifications, so tell the table view to process all updates.
    @try {
        if (self.theTableView != nil) {
            //[self.theTableView endUpdates];
            if (self.moving) {
                self.moving = NO;
                [self.theTableView reloadData];
                //[self performSelector:@selector(reloadData) withObject:nil afterDelay:0.02];
            }
            [self performSelector:@selector(save) withObject:nil afterDelay:0.02];
        }   

    }
    @catch (NSException * e) {
        debug_NSLog(@"Failure in controllerDidChangeContent, name=%@ reason=%@", e.name, e.reason);
    }
}
person Greg Combs    schedule 21.07.2009
comment
Спасибо, Грег, приведенный выше код у меня работает. Мне нужно изменить код controllerDidChangeContent, чтобы добавить в endUpdates, чтобы удалить и вставить работу для обновления пользовательского интерфейса: if (self.moving) {self.moving = NO; [self.theTableView reloadData]; [самостоятельно выполнитьSelector: @selector (сохранить) withObject: nil afterDelay: 0,02]; } еще {[self.theTableView endUpdates]; } - person Gaius Parx; 05.11.2009

На самом деле лучший ответ - в комментарии Клинта Харриса по этому вопросу:

http://www.cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller

Чтобы быстро подвести итог, необходимо иметь свойство displayOrder для объектов, которые вы пытаетесь изменить, с описанием сортировки для упорядочивания полученного контроллера результатов в этом поле. Тогда код для moveRowAtIndexPath:toIndexPath: выглядит так:

- (void)tableView:(UITableView *)tableView 
moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath 
      toIndexPath:(NSIndexPath *)destinationIndexPath;
{  
  NSMutableArray *things = [[fetchedResultsController fetchedObjects] mutableCopy];

  // Grab the item we're moving.
  NSManagedObject *thing = [[self fetchedResultsController] objectAtIndexPath:sourceIndexPath];

  // Remove the object we're moving from the array.
  [things removeObject:thing];
  // Now re-insert it at the destination.
  [things insertObject:thing atIndex:[destinationIndexPath row]];

  // All of the objects are now in their correct order. Update each
  // object's displayOrder field by iterating through the array.
  int i = 0;
  for (NSManagedObject *mo in things)
  {
    [mo setValue:[NSNumber numberWithInt:i++] forKey:@"displayOrder"];
  }

  [things release], things = nil;

  [managedObjectContext save:nil];
}

Документация Apple также содержит важные подсказки:

https://developer.apple.com/library/ios/#documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html

Это также упоминается в Как реализовать изменение порядка записей CoreData? < / а>

Процитируем документацию Apple:

Пользовательские обновления

В общем, NSFetchedResultsController разработан, чтобы реагировать на изменения на уровне модели. Если вы разрешаете пользователю переупорядочивать строки таблицы, то ваша реализация методов делегата должна учитывать это.

Обычно, если вы разрешаете пользователю переупорядочивать строки таблицы, ваш объект модели имеет атрибут, определяющий его индекс. Когда пользователь перемещает строку, вы соответствующим образом обновляете этот атрибут. Это, однако, имеет побочный эффект, заставляя контроллер заметить изменение и, таким образом, сообщить своему делегату об обновлении (используя controller: didChangeObject: atIndexPath: forChangeType: newIndexPath :). Если вы просто используете реализацию этого метода, показанную в разделе «Типичное использование», тогда делегат попытается обновить табличное представление. Однако табличное представление уже находится в соответствующем состоянии из-за действий пользователя.

В общем, поэтому, если вы поддерживаете обновления, управляемые пользователем, вы должны установить флаг, если перемещение инициировано пользователем. В реализации методов делегата, если установлен флаг, вы обходите реализации основных методов; Например:

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

    if (!changeIsUserDriven) {
        UITableView *tableView = self.tableView;
        // Implementation continues...
person JosephH    schedule 03.10.2012
comment
этот forin things setValue... невероятно прост и спас мне кучу кода, который пытался быть более эффективным, но не работал должным образом ... НО - насколько это неэффективно? Скажем, менее 200 объектов нужно ли мне заботиться об эффективности здесь? - person Aviel Gross; 08.05.2014
comment
@AvielGross Я думаю, что обновление 200 объектов один раз в ответ на действие пользователя не вызовет заметных накладных расходов. Единственная часть, которую вам, возможно, придется наблюдать, - это managedObjectContext save - тест на самом медленном устройстве, на которое вы нацеливаетесь, и если вы обнаружите какие-либо проблемы, вам может потребоваться сохранить в фоновом потоке или отложить его. - person JosephH; 08.05.2014
comment
Итак, если мой moc создан с помощью NSMainQueueConcurrencyType, я могу просто вызвать метод save: внутри [context performBlock...]? Или performBlockAndWait...? - person Aviel Gross; 08.05.2014
comment
@AvielGross Я думаю, что сейчас мы уходим от темы этого вопроса; Я предлагаю задать новый вопрос, но чтобы ответить, ни один из этих подходов не поможет, поскольку NSMainQueueConcurrencyType означает, что они будут запускать блок в основном потоке, а этот код уже выполняется в основном потоке. Если производительность является проблемой, вы можете переместить сохранение на диск / флэш-память в фоновый поток, но многопоточные общие данные - это большая и сложная тема. - person JosephH; 08.05.2014

Когда вы перемещаете строку в представлении таблицы, вы фактически перемещаете блок других строк (состоящий хотя бы из одной строки) в другом направлении одновременно. Хитрость заключается в том, чтобы обновить только свойство displayOrder этого блока и перемещенного элемента.

Во-первых, убедитесь, что свойство displayOrder всех строк установлено в соответствии с текущим порядком отображения таблиц. Здесь нам не нужно сохранять контекст, мы сохраним его позже, когда фактическая операция перемещения завершится:

- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
    [super setEditing:editing animated:animated];
    [_tableView setEditing:editing animated:animated];
    if(editing) {
        NSInteger rowsInSection = [self tableView:_tableView numberOfRowsInSection:0];
       // Update the position of all items
       for (NSInteger i=0; i<rowsInSection; i++) {
          NSIndexPath *curIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
          SomeManagedObject *curObj = [_fetchedResultsController objectAtIndexPath:curIndexPath];
          NSNumber *newPosition = [NSNumber numberWithInteger:i];
          if (![curObj.displayOrder isEqualToNumber:newPosition]) {
             curObj.displayOrder = newPosition;
          }
       }
    }
}

Тогда единственное, что вам нужно сделать, это обновить положение перемещенного элемента и всех элементов между fromIndexPath и toIndexPath:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
    NSInteger moveDirection = 1;
    NSIndexPath *lowerIndexPath = toIndexPath;
    NSIndexPath *higherIndexPath = fromIndexPath;
    if (fromIndexPath.row < toIndexPath.row) {
        // Move items one position upwards
        moveDirection = -1;
        lowerIndexPath = fromIndexPath;
        higherIndexPath = toIndexPath;
    }

    // Move all items between fromIndexPath and toIndexPath upwards or downwards by one position
    for (NSInteger i=lowerIndexPath.row; i<=higherIndexPath.row; i++) {
        NSIndexPath *curIndexPath = [NSIndexPath indexPathForRow:i inSection:fromIndexPath.section];
        SomeManagedObject *curObj = [_fetchedResultsController objectAtIndexPath:curIndexPath];
        NSNumber *newPosition = [NSNumber numberWithInteger:i+moveDirection];
        curObj.displayOrder = newPosition;
    }

    SomeManagedObject *movedObj = [_fetchedResultsController objectAtIndexPath:fromIndexPath];
    movedObj.displayOrder = [NSNumber numberWithInteger:toIndexPath.row];
    NSError *error;
    if (![_fetchedResultsController.managedObjectContext save:&error]) {
        NSLog(@"Could not save context: %@", error);
    }
}
person Tom    schedule 29.01.2012

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{
    [self.pairs exchangeObjectAtIndex:sourceIndexPath.row withObjectAtIndex:destinationIndexPath.row];
    [self performSelector:@selector(reloadData) withObject:nil afterDelay:0.02];
}

- (void)reloadData{
    [table reloadData];
}

Стол не может перезагружаться во время движения, перезагрузите его после задержки, и все будет в порядке.

person Community    schedule 28.07.2009
comment
Это все еще приносило исключения, см. Мой обновленный ответ о том, что сейчас работает. - person Greg Combs; 29.07.2009
comment
Извините, если бы я знал, что происходит. Я всегда использую для этого изменяемые массивы, которые кажутся менее сложными, но на самом деле не очень хорошо подходят для вашего образца кода. вздох - person ; 30.07.2009
comment
Почему бы вам не сделать это вместо этого [table performSelector:@selector(reloadData) withObject:nil afterDelay:0.02];, чтобы вам не пришлось создавать другой метод :) - person Sam Soffes; 12.02.2010
comment
это сработало, но кажется не очень хорошим эффектом, весь вид будет обновлен. выглядит, что ответ выбора в этом потоке отличный, но я думаю, что «вставить» и «удалить» не нужно, я просто использую поле displayIndex для управления порядком, если строка перемещается на другую позицию в списке, я просто используйте способ пузырьковой сортировки, измените значение поля, это отлично работает, не требуется «вставка» и «удаление» - person iXcoder; 29.05.2010

Извини, Грег, я уверен, что делаю что-то не так, но твой ответ мне не подходит.

Хотя все мои объекты проходят проверку правильно, когда я выхожу из режима редактирования, одна из строк зависает (элементы управления редактированием не исчезают), и впоследствии ячейки не реагируют должным образом.

Возможно, моя проблема в том, что я не знаю, как использовать заданное вами свойство moving (self.moving = YES). Не могли бы вы прояснить это? Большое Вам спасибо.

Хорхе

person Jorge Ortiz    schedule 15.09.2009
comment
Я видел то же самое раньше. К сожалению, я неделями не копался в своих источниках и больше не держу их здесь на работе. Я посмотрю, смогу ли я отследить, как я использую self.moving, но если я помню, это было отложить обновление основного хранилища данных / полученных результатов до завершения редактирования / перемещения. После этого я убеждаюсь, что свойство order соответствует тому, что должно быть, затем обновляю основное хранилище данных / получаю результаты, затем снова загружаю / обновляю данные, отображаемые в таблице, СНОВА, чтобы визуальные несоответствия были устранены. Это только по памяти, проверю. - person Greg Combs; 16.09.2009
comment
Глядя на все сейчас ... Инициализируйте переход на НЕТ. Установите перемещение на YES и validateLinkOrder, когда мы обнаружим didObJectChange / NSFetchedResultsChangeMove. Ничего не делайте в controllerWillChangeContent. Но в controllerDidChangeContent посмотрите, является ли перемещение ДА, если да, мы устанавливаем его в NO, сообщаем tableView reloadData и выполняем селектор сохранения после задержки 0,02. Селектор сохранения в основном сообщает managedObjectContext сохранять и регистрировать любые ошибки, но он делает это в рамках обработки исключений try / catch, если что-то пойдет не так. - person Greg Combs; 21.09.2009
comment
См. Мой измененный листинг кода в проверенном ответе выше. - person Greg Combs; 21.09.2009
comment
Наконец-то он заработал. Моя проблема заключалась в том, что в атрибуте order вместо NSNumber хранилось int. Эти сбои Core Data довольно уродливы, а информация бесполезна. Большое спасибо!!! - person Jorge Ortiz; 25.09.2009

Это очень сложный способ реализовать такую ​​функциональность. Здесь можно найти гораздо более простой и элегантный способ: переупорядочение основных данных UITableView

person AlexS    schedule 17.03.2010
comment
Ну, естественно, все становится на много проще, если вы не используете fetchedResultsController с переупорядочиванием. По крайней мере, проще, когда дело доходит до переупорядочивания. Я полагаю, если администрация AppleDev рекомендует использовать fetchedResultsController, тогда у нас должен быть изящный способ перегруппировки. К сожалению, кажется, что, хотя эти две вещи совместимы, как показано выше, они определенно не изящны, как показано выше. - person Greg Combs; 18.03.2010

[‹> IsEditing] может использоваться, чтобы определить, разрешено ли редактирование таблицы или нет. Вместо того, чтобы откладывать это, как предлагается, используя следующий оператор

[table performSelector: @selector (reloadData) withObject: nil afterDelay: 0,02];

person Sangappa Paraddi    schedule 17.11.2010