2009-07-20 4 views
23

Возможный дубликат:
How to implement re-ordering of CoreData records?Переупорядочивание UITableView с Core Data

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

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

WTF. Мне нужно каким-то образом аннулировать 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]; 
    } 
} 
+0

Я работал на это немного по-другому (разместим, когда оно испытано). Для этого действительно нужен «Reverse-NSFetchedResultsController». По существу загружайте NSFetchedResultsController UITableView, порядок сортировки и заголовки разделов, и все это записывайте обратно в CoreData. Я думаю, я мог бы попытаться собрать это вместе для удовольствия. –

+0

Абсолютно ... что-то немного более элегантное, чем мое решение, которое позволит двунаправленную синхронизацию между сохраненными данными и пользовательским интерфейсом таблицы. Я бы с удовольствием посмотрел, что вы придумали, когда закончите. –

+4

Для чего это стоит, Мэтт Лонг недавно опубликовал хороший учебник по переупорядочению строк в таблице NSFetchedResultsController: http://www.cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller –

ответ

7

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

- (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); 
    } 
} 
+0

Спасибо, Грег, приведенный выше код работает для меня. Мне нужно изменить код контроллераDidChangeContent для добавления в endUpdates для удаления и вставки работы для обновления ui: if (self.moving) { self.moving = NO; [self.theTableView reloadData]; [self performSelector: @selector (save) withObject: nil afterDelay: 0.02]; } else { [self.theTableView endUpdates]; } –

8

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

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

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

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

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

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

+0

Большое вам спасибо! – Mason

1
- (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]; 
} 

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

+0

Это все еще вызывало исключения, см. Мой обновленный «ответ» за то, что сейчас работает. –

+0

Жаль, что я не знал, что происходит. Я всегда использую переменные массивы для этого, что кажется менее сложным, но на самом деле не очень хорошо служит вашему образцу кода. * sigh * – 2009-07-30 19:27:33

+0

Почему бы вам не сделать это вместо '[table performSelector: @selector (reloadData) withObject: noil afterDelay: 0.02];' поэтому вам не нужно делать другой метод :) –

1

Извините, Грег, я уверен, что я делаю что-то неправильно, но вы отвечаете, что не работает для меня.

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

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

Jorge

+0

Я видел это точно так же, как раньше. К сожалению, я не зарылся в свои источники за несколько недель, и я больше не буду их здесь на работе. Я посмотрю, смогу ли я отследить, как я использую self.moving, но если я помню, это должно было задержать обновление основных данных хранения данных/полученных результатов до тех пор, пока редактирование/перемещение не завершится. После этого я убеждаюсь, что свойство «order» согласуется с тем, что должно быть, а затем обновлять основные хранилища данных/извлеченные результаты, а затем перезагружать/обновлять данные, отображаемые в таблице AGAIN, чтобы очистить визуальные несоответствия. Это только из памяти, я проверю. –

+0

Посмотрите на все сейчас ... Инициализируйте переход в НЕТ. Установите переход к YES и validateLinkOrder, когда мы обнаружим didObJectChange/NSFetchedResultsChangeMove. Не делайте ничего в контроллереWillChangeContent. Но в контроллереDidChangeContent см., Если движется ДА, если это так, мы устанавливаем его в НЕТ, рассказываем tableView о перезагрузке данных и выполняем селектор «save» после задержки 0,02. Селектор «save» в основном сообщает управляемомуObjectContext сохранять и записывать любые ошибки, но он делает это в обработке исключений try/catch, если все происходит плохо. –

+0

См. Мой пересмотренный список кода в ответе «checked» выше. –

1

Это очень сложный способ реализации таких функций. Гораздо проще и элегантный способ можно найти здесь: UITableView Core Data reordering

+1

Ну, естественно, вещи становятся намного проще, если вы не используете fetchedResultsController с переупорядочением. По крайней мере, проще, когда речь заходит о переупорядочивающей его части. Я полагаю, что при использовании исправленногоResultsController рекомендуется администратором AppleDev, тогда мы должны иметь изящный способ перестановки. К сожалению, кажется, что, хотя эти две вещи совместимы, как показано выше, они, конечно, не изящны, как показано выше. –

1

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

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

5

При перемещении строки в виде таблицы вы фактически перемещаете блок других строк (состоящий хотя бы из одной строки) в другое направление одновременно. Хитрость заключается только в обновлении свойства 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); 
    } 
} 
5

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

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

Это также упоминается в How to implement re-ordering of CoreData records?

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

Пользовательские данные, Driven Updates

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

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

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

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

    if (!changeIsUserDriven) { 
     UITableView *tableView = self.tableView; 
     // Implementation continues... 
+0

этот 'forin things setValue ...' невероятно прост и сэкономил мне кучу кода, который пытался быть более эффективным, но не работал должным образом ... НО - насколько это не эффективно? Скажем, до 200 объектов мне нужно заботиться об эффективности здесь? –

+0

@AvielGross Я думаю, что обновление 200 объектов однажды в ответ на действие пользователя не будет заметным накладным. Единственная часть, которую вам, возможно, придется наблюдать, - это тест «managedObjectContext save» на самом медленном устройстве, на которое вы нацеливаетесь, и если вы видите какие-либо проблемы, вам может потребоваться сохранить фоновый поток или отложить его. – JosephH

+0

Итак, если мой 'moc' создан с помощью' NSMainQueueConcurrencyType', я могу просто вызвать метод 'save:' внутри '[context performBlock ...]'? Или 'performBlockAndWait ...'? –

Смежные вопросы